Files
mixvideo-v2/apps/desktop/src/pages/Tools.tsx
imeepos 78d9296155 feat: 实现 Topaz Video AI 完整功能和真实 FFmpeg 进度监控
新功能:
- 完整的 Topaz Video AI 参数配置界面
- 真实的 FFmpeg 执行进度条(非模拟)
- 一键视频处理功能
- 详细的错误处理和日志记录

 技术改进:
- 修复 FFmpeg 命令格式问题
- 解决参数类型转换错误
- 优化命令行参数解析
- 添加双事件系统支持进度监控

 问题修复:
- 修复元数据引用问题
- 解决配置参数冲突
- 修复前后端数据格式不匹配
- 优化错误信息显示

 文件变更:
- 新增 TopazVideoAIConfigurator 组件
- 新增 topazTemplateService 服务
- 更新 tvai_commands 后端命令
- 优化 topaz_templates 配置
- 完善 web_api 接口
2025-08-14 14:59:21 +08:00

255 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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, useMemo } from 'react';
import {
Wrench,
Search,
Grid,
List,
Star,
Sparkles
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { CardGrid } from '../components/CardGrid';
import ToolCard from '../components/ToolCard';
import { TOOLS_DATA, TOOL_CATEGORIES, searchTools } from '../data/tools';
import { Tool, ToolCategory } from '../types/tool';
/**
* 便捷小工具页面
* 遵循 Tauri 开发规范和 UI/UX 设计标准
* 使用卡片列表展示工具,点击进入详情页
*/
const Tools: React.FC = () => {
const navigate = useNavigate();
// 工具列表状态
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<ToolCategory | null>(null);
const [showNewOnly, setShowNewOnly] = useState(false);
const [showPopularOnly, setShowPopularOnly] = useState(false);
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
// 根据筛选条件过滤工具列表
const filteredTools = useMemo(() => {
let result = [...TOOLS_DATA];
// 搜索过滤
if (searchQuery) {
result = searchTools(searchQuery);
}
// 分类过滤
if (selectedCategory) {
result = result.filter(tool => tool.category === selectedCategory);
}
// 新功能过滤
if (showNewOnly) {
result = result.filter(tool => tool.isNew);
}
// 热门工具过滤
if (showPopularOnly) {
result = result.filter(tool => tool.isPopular);
}
return result;
}, [searchQuery, selectedCategory, showNewOnly, showPopularOnly]);
// 处理工具卡片点击
const handleToolClick = (tool: Tool) => {
navigate(tool.route);
};
// 清除所有筛选条件
const clearFilters = () => {
setSearchQuery('');
setSelectedCategory(null);
setShowNewOnly(false);
setShowPopularOnly(false);
};
return (
<div className="space-y-6">
{/* 页面标题 */}
<div className="page-header flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg hover:shadow-xl transition-all duration-300">
<Wrench className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold bg-gradient-to-r from-gray-900 to-purple-600 bg-clip-text text-transparent"></h1>
<p className="text-gray-600 text-lg"></p>
</div>
</div>
{/* 视图切换 */}
<div className="flex items-center gap-2">
<button
onClick={() => setViewMode('grid')}
className={`p-2 rounded-lg transition-colors ${
viewMode === 'grid'
? 'bg-primary-100 text-primary-600'
: 'text-gray-400 hover:text-gray-600'
}`}
>
<Grid className="w-5 h-5" />
</button>
<button
onClick={() => setViewMode('list')}
className={`p-2 rounded-lg transition-colors ${
viewMode === 'list'
? 'bg-primary-100 text-primary-600'
: 'text-gray-400 hover:text-gray-600'
}`}
>
<List className="w-5 h-5" />
</button>
</div>
</div>
{/* 搜索和筛选区域 */}
<div className="card p-6 animate-fade-in">
<div className="flex flex-col lg:flex-row gap-4">
{/* 搜索框 */}
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="text"
placeholder="搜索工具名称、描述或标签..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-all duration-200 bg-gray-50 hover:bg-white"
/>
</div>
</div>
{/* 分类筛选 */}
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedCategory(null)}
className={`px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200 ${
selectedCategory === null
? 'bg-primary-500 text-white shadow-md hover:bg-primary-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 hover:shadow-sm'
}`}
>
</button>
{TOOL_CATEGORIES.map((category) => (
<button
key={category.id}
onClick={() => setSelectedCategory(category.id)}
className={`px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200 ${
selectedCategory === category.id
? 'bg-primary-500 text-white shadow-md hover:bg-primary-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 hover:shadow-sm'
}`}
>
{category.name}
</button>
))}
</div>
{/* 快速筛选 */}
<div className="flex gap-2">
<button
onClick={() => setShowNewOnly(!showNewOnly)}
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200 ${
showNewOnly
? 'bg-green-500 text-white shadow-md hover:bg-green-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 hover:shadow-sm'
}`}
>
<Sparkles className="w-4 h-4" />
</button>
<button
onClick={() => setShowPopularOnly(!showPopularOnly)}
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200 ${
showPopularOnly
? 'bg-yellow-500 text-white shadow-md hover:bg-yellow-600'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 hover:shadow-sm'
}`}
>
<Star className="w-4 h-4" />
</button>
</div>
</div>
{/* 筛选结果统计 */}
<div className="mt-6 pt-4 border-t border-gray-100 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900"> {filteredTools.length} </span>
{filteredTools.length !== TOOLS_DATA.length && (
<span className="text-xs text-gray-500"> {TOOLS_DATA.length} </span>
)}
</div>
{(searchQuery || selectedCategory || showNewOnly || showPopularOnly) && (
<button
onClick={clearFilters}
className="px-3 py-1.5 text-sm font-medium text-primary-600 hover:text-primary-700 hover:bg-primary-50 rounded-lg transition-all duration-200"
>
</button>
)}
</div>
</div>
{/* 工具卡片列表 */}
<div className="animate-fade-in">
<CardGrid
items={filteredTools}
renderCard={(tool) => (
<ToolCard
key={tool.id}
tool={tool}
onClick={handleToolClick}
config={{
showStatus: true,
showCategory: true,
showTags: false,
showVersion: true,
size: 'medium',
enableHover: true
}}
/>
)}
loading={false}
searchable={false}
sortable={false}
viewModes={[viewMode]}
defaultViewMode={viewMode}
gridCols={{
sm: 1,
md: 2,
lg: 3,
xl: 3,
'2xl': 4
}}
gap={6}
emptyText="没有找到匹配的工具"
emptyComponent={
<div className="text-center py-16">
<div className="w-24 h-24 bg-gradient-to-br from-gray-100 to-gray-200 rounded-full flex items-center justify-center mx-auto mb-6">
<Wrench className="w-12 h-12 text-gray-400" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3"></h3>
<p className="text-gray-600 mb-6 max-w-md mx-auto"></p>
<button
onClick={clearFilters}
className="px-6 py-3 bg-primary-600 text-white rounded-xl hover:bg-primary-700 transition-all duration-200 shadow-md hover:shadow-lg font-medium"
>
</button>
</div>
}
/>
</div>
</div>
);
};
export default Tools;