forked from 77media/video-flow
- 新增任务统计数据接口和消息队列状态接口 - 添加任务监控页面 (app/task-monitor/page.tsx) - 实现任务监控相关组件: - 项目选择器 (project-selector.tsx) - 任务统计卡片 (task-statistics-cards.tsx) - 任务状态图表 (task-status-chart.tsx) - 任务时间线图表 (task-timeline-chart.tsx) - 任务详情表格 (task-detail-table.tsx) - 消息队列面板 (message-queue-panel.tsx) - 扩展 video_flow.ts API 接口,支持项目任务统计和消息队列管理
220 lines
7.5 KiB
TypeScript
220 lines
7.5 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { cn } from '@/public/lib/utils';
|
|
|
|
interface Project {
|
|
id: string;
|
|
name: string;
|
|
status: 'active' | 'inactive';
|
|
last_activity: string;
|
|
}
|
|
|
|
interface ProjectSelectorProps {
|
|
selectedProjectIds: string[];
|
|
onProjectChange: (projectIds: string[]) => void;
|
|
}
|
|
|
|
export default function ProjectSelector({ selectedProjectIds, onProjectChange }: ProjectSelectorProps) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
// 模拟项目数据 - 在实际应用中应该从API获取
|
|
const mockProjects: Project[] = [
|
|
{
|
|
id: 'bc43bc81-c781-4caa-8256-9710fd5bee80',
|
|
name: '视频制作项目 A',
|
|
status: 'active',
|
|
last_activity: '2024-01-15T10:30:00Z'
|
|
},
|
|
{
|
|
id: 'd203016d-6f7e-4d1c-b66b-1b7d33632800',
|
|
name: '视频制作项目 B',
|
|
status: 'active',
|
|
last_activity: '2024-01-15T09:15:00Z'
|
|
},
|
|
{
|
|
id: '029bbc09-6d83-440b-97fe-e2aa37b8042d',
|
|
name: '视频制作项目 C',
|
|
status: 'active',
|
|
last_activity: '2024-01-14T16:45:00Z'
|
|
},
|
|
{
|
|
id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
|
name: '视频制作项目 D',
|
|
status: 'inactive',
|
|
last_activity: '2024-01-10T14:20:00Z'
|
|
},
|
|
];
|
|
|
|
useEffect(() => {
|
|
// 模拟加载项目列表
|
|
setLoading(true);
|
|
setTimeout(() => {
|
|
setProjects(mockProjects);
|
|
setLoading(false);
|
|
}, 500);
|
|
}, []);
|
|
|
|
const filteredProjects = projects.filter(project =>
|
|
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
project.id.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
const handleProjectToggle = (projectId: string) => {
|
|
if (selectedProjectIds.includes(projectId)) {
|
|
// 移除项目
|
|
onProjectChange(selectedProjectIds.filter(id => id !== projectId));
|
|
} else {
|
|
// 添加项目
|
|
onProjectChange([...selectedProjectIds, projectId]);
|
|
}
|
|
};
|
|
|
|
const handleSelectAll = () => {
|
|
const activeProjects = filteredProjects.filter(p => p.status === 'active');
|
|
onProjectChange(activeProjects.map(p => p.id));
|
|
};
|
|
|
|
const handleClearAll = () => {
|
|
onProjectChange([]);
|
|
};
|
|
|
|
const getSelectedProjectNames = () => {
|
|
return selectedProjectIds
|
|
.map(id => projects.find(p => p.id === id)?.name || id.slice(0, 8))
|
|
.join(', ');
|
|
};
|
|
|
|
return (
|
|
<div className="relative">
|
|
{/* 选择器按钮 */}
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="flex items-center gap-2 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white hover:bg-gray-700 transition-colors min-w-[200px]"
|
|
>
|
|
<div className="flex-1 text-left truncate">
|
|
{selectedProjectIds.length === 0 ? (
|
|
<span className="text-gray-400">选择项目...</span>
|
|
) : selectedProjectIds.length === 1 ? (
|
|
<span>{getSelectedProjectNames()}</span>
|
|
) : (
|
|
<span>{selectedProjectIds.length} 个项目</span>
|
|
)}
|
|
</div>
|
|
<svg
|
|
className={cn('w-4 h-4 transition-transform', isOpen && 'rotate-180')}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
{/* 下拉面板 */}
|
|
{isOpen && (
|
|
<div className="absolute top-full left-0 mt-2 w-80 bg-gray-800 border border-gray-700 rounded-lg shadow-xl z-50">
|
|
{/* 搜索框 */}
|
|
<div className="p-3 border-b border-gray-700">
|
|
<input
|
|
type="text"
|
|
placeholder="搜索项目..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded text-white placeholder-gray-400 text-sm focus:outline-none focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* 批量操作 */}
|
|
<div className="p-3 border-b border-gray-700 flex gap-2">
|
|
<button
|
|
onClick={handleSelectAll}
|
|
className="px-3 py-1 bg-blue-500 hover:bg-blue-600 text-white rounded text-sm transition-colors"
|
|
>
|
|
全选活跃
|
|
</button>
|
|
<button
|
|
onClick={handleClearAll}
|
|
className="px-3 py-1 bg-gray-600 hover:bg-gray-500 text-white rounded text-sm transition-colors"
|
|
>
|
|
清空
|
|
</button>
|
|
<div className="flex-1 text-right text-sm text-gray-400 flex items-center justify-end">
|
|
已选 {selectedProjectIds.length} 个
|
|
</div>
|
|
</div>
|
|
|
|
{/* 项目列表 */}
|
|
<div className="max-h-64 overflow-y-auto">
|
|
{loading ? (
|
|
<div className="p-4 text-center text-gray-400">
|
|
<div className="animate-spin w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full mx-auto mb-2"></div>
|
|
加载中...
|
|
</div>
|
|
) : filteredProjects.length === 0 ? (
|
|
<div className="p-4 text-center text-gray-400">
|
|
{searchTerm ? '未找到匹配的项目' : '暂无项目'}
|
|
</div>
|
|
) : (
|
|
<div className="p-2">
|
|
{filteredProjects.map((project) => (
|
|
<div
|
|
key={project.id}
|
|
className={cn(
|
|
'flex items-center gap-3 p-2 rounded hover:bg-gray-700 cursor-pointer transition-colors',
|
|
selectedProjectIds.includes(project.id) && 'bg-blue-500/20'
|
|
)}
|
|
onClick={() => handleProjectToggle(project.id)}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedProjectIds.includes(project.id)}
|
|
onChange={() => {}} // 由父级点击处理
|
|
className="rounded"
|
|
/>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-white font-medium truncate">{project.name}</span>
|
|
<div className={cn(
|
|
'w-2 h-2 rounded-full',
|
|
project.status === 'active' ? 'bg-emerald-400' : 'bg-gray-500'
|
|
)}></div>
|
|
</div>
|
|
<div className="text-xs text-gray-400 truncate">
|
|
ID: {project.id}
|
|
</div>
|
|
<div className="text-xs text-gray-500">
|
|
最后活动: {new Date(project.last_activity).toLocaleDateString()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 底部信息 */}
|
|
<div className="p-3 border-t border-gray-700 text-xs text-gray-400">
|
|
<div className="flex items-center justify-between">
|
|
<span>共 {projects.length} 个项目</span>
|
|
<span>支持多选对比</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 点击外部关闭 */}
|
|
{isOpen && (
|
|
<div
|
|
className="fixed inset-0 z-40"
|
|
onClick={() => setIsOpen(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|