video-flow-b/components/task-monitor/project-selector.tsx
qikongjian 9258f50f8e feat: 添加任务监控功能
- 新增任务统计数据接口和消息队列状态接口
- 添加任务监控页面 (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 接口,支持项目任务统计和消息队列管理
2025-08-26 22:03:57 +08:00

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>
);
}