fix: 修复前端队列控制按钮功能
问题修复: - 修复暂停/恢复/停止按钮无法正常工作的问题 - 添加详细的调试日志来诊断按钮操作 - 优化按钮的启用/禁用逻辑和视觉反馈 技术改进: - 在队列操作后添加延迟确保后端状态更新 - 修复refreshQueueStatus方法的返回值类型 - 改进按钮状态判断逻辑,支持更多队列状态 用户体验提升: - 添加状态指示器显示当前队列状态 - 改进按钮的禁用状态样式 - 提供更清晰的按钮提示文本 状态管理优化: - 确保队列控制操作后正确刷新状态 - 添加操作中的加载状态指示 - 改进错误处理和日志输出 任务恢复机制: - 添加recover_stuck_tasks功能恢复卡住的任务 - 在队列启动时自动恢复处理中状态的任务 - 解决应用意外关闭导致任务永久卡住的问题
This commit is contained in:
parent
0c7eeb9905
commit
7a9ac750ae
|
|
@ -85,6 +85,18 @@ impl VideoClassificationQueue {
|
|||
*status = QueueStatus::Running;
|
||||
drop(status);
|
||||
|
||||
// 恢复卡住的任务状态
|
||||
match self.service.recover_stuck_tasks().await {
|
||||
Ok(recovered_count) => {
|
||||
if recovered_count > 0 {
|
||||
println!("🔄 队列启动时恢复了 {} 个卡住的任务", recovered_count);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("⚠️ 恢复卡住任务时出错: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
self.update_stats().await?;
|
||||
|
||||
|
|
|
|||
|
|
@ -305,6 +305,11 @@ impl VideoClassificationService {
|
|||
self.video_repo.delete_task(task_id).await
|
||||
}
|
||||
|
||||
/// 恢复卡住的任务状态
|
||||
pub async fn recover_stuck_tasks(&self) -> Result<usize> {
|
||||
self.video_repo.recover_stuck_tasks().await
|
||||
}
|
||||
|
||||
/// 重试失败的任务
|
||||
pub async fn retry_failed_task(&self, _task_id: &str) -> Result<()> {
|
||||
// 这里需要实现重试逻辑
|
||||
|
|
|
|||
|
|
@ -472,6 +472,25 @@ impl VideoClassificationRepository {
|
|||
let completed_json = serde_json::to_string(&TaskStatus::Completed)?;
|
||||
let failed_json = serde_json::to_string(&TaskStatus::Failed)?;
|
||||
|
||||
// 调试:查看数据库中实际的任务状态分布
|
||||
let debug_query = if let Some(project_id) = project_id {
|
||||
format!("SELECT status, COUNT(*) FROM video_classification_tasks WHERE project_id = '{}' GROUP BY status", project_id)
|
||||
} else {
|
||||
"SELECT status, COUNT(*) FROM video_classification_tasks GROUP BY status".to_string()
|
||||
};
|
||||
|
||||
let mut debug_stmt = conn.prepare(&debug_query)?;
|
||||
let debug_rows = debug_stmt.query_map([], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, i32>(1)?))
|
||||
})?;
|
||||
|
||||
println!("🔍 数据库中实际的任务状态分布:");
|
||||
for row in debug_rows {
|
||||
if let Ok((status, count)) = row {
|
||||
println!(" 状态: {} -> 数量: {}", status, count);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取任务统计
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"SELECT
|
||||
|
|
@ -484,13 +503,12 @@ impl VideoClassificationRepository {
|
|||
))?;
|
||||
|
||||
let task_stats = stmt.query_row([&pending_json, &uploading_json, &analyzing_json, &completed_json, &failed_json], |row| {
|
||||
Ok((
|
||||
row.get::<_, i32>(0)?,
|
||||
row.get::<_, i32>(1)?,
|
||||
row.get::<_, i32>(2)?,
|
||||
row.get::<_, i32>(3)?,
|
||||
row.get::<_, i32>(4)?,
|
||||
))
|
||||
let total: i32 = row.get(0)?;
|
||||
let pending: i32 = row.get(1)?;
|
||||
let processing: i32 = row.get(2)?;
|
||||
let completed: i32 = row.get(3)?;
|
||||
let failed: i32 = row.get(4)?;
|
||||
Ok((total, pending, processing, completed, failed))
|
||||
})?;
|
||||
|
||||
// 获取分类记录统计
|
||||
|
|
@ -544,6 +562,49 @@ impl VideoClassificationRepository {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// 恢复卡住的任务状态
|
||||
/// 将所有处理中的任务(Uploading, Analyzing)重置为Pending状态
|
||||
pub async fn recover_stuck_tasks(&self) -> Result<usize> {
|
||||
let conn = self.database.get_connection();
|
||||
let conn = conn.lock().unwrap();
|
||||
|
||||
let uploading_json = serde_json::to_string(&TaskStatus::Uploading)?;
|
||||
let analyzing_json = serde_json::to_string(&TaskStatus::Analyzing)?;
|
||||
let pending_json = serde_json::to_string(&TaskStatus::Pending)?;
|
||||
|
||||
println!("🔄 开始恢复卡住的任务状态...");
|
||||
|
||||
// 查询卡住的任务
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, status FROM video_classification_tasks WHERE status = ? OR status = ?"
|
||||
)?;
|
||||
|
||||
let stuck_tasks: Vec<(String, String)> = stmt.query_map([&uploading_json, &analyzing_json], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||
})?.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
if stuck_tasks.is_empty() {
|
||||
println!("✅ 没有发现卡住的任务");
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
println!("🔍 发现 {} 个卡住的任务:", stuck_tasks.len());
|
||||
for (id, status) in &stuck_tasks {
|
||||
println!(" 任务ID: {}, 状态: {}", &id[..8], status);
|
||||
}
|
||||
|
||||
// 重置任务状态
|
||||
let updated = conn.execute(
|
||||
"UPDATE video_classification_tasks
|
||||
SET status = ?, started_at = NULL, updated_at = datetime('now')
|
||||
WHERE status = ? OR status = ?",
|
||||
[&pending_json, &uploading_json, &analyzing_json]
|
||||
)?;
|
||||
|
||||
println!("✅ 已恢复 {} 个任务状态为Pending", updated);
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
/// 根据ID获取分类任务
|
||||
pub async fn get_task_by_id(&self, task_id: &str) -> Result<Option<VideoClassificationTask>> {
|
||||
let conn = self.database.get_connection();
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ pub fn run() {
|
|||
commands::video_classification_commands::get_all_classification_task_progress,
|
||||
commands::video_classification_commands::get_project_classification_task_progress,
|
||||
commands::video_classification_commands::stop_classification_queue,
|
||||
commands::video_classification_commands::recover_stuck_classification_tasks,
|
||||
commands::video_classification_commands::pause_classification_queue,
|
||||
commands::video_classification_commands::resume_classification_queue,
|
||||
commands::video_classification_commands::get_material_classification_records,
|
||||
|
|
|
|||
|
|
@ -118,6 +118,28 @@ pub async fn stop_classification_queue(
|
|||
queue.stop().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 恢复卡住的任务状态
|
||||
#[command]
|
||||
pub async fn recover_stuck_classification_tasks(
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<usize, String> {
|
||||
let database = state.get_database();
|
||||
let video_repo = Arc::new(VideoClassificationRepository::new(database.clone()));
|
||||
let ai_classification_repo = Arc::new(AiClassificationRepository::new(database.clone()));
|
||||
let material_repo = Arc::new(MaterialRepository::new(database.get_connection()).unwrap());
|
||||
|
||||
let service = VideoClassificationService::new(
|
||||
video_repo,
|
||||
ai_classification_repo,
|
||||
material_repo,
|
||||
Some(GeminiConfig::default()),
|
||||
);
|
||||
|
||||
service.recover_stuck_tasks()
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 暂停分类队列
|
||||
#[command]
|
||||
pub async fn pause_classification_queue(
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ const Navigation: React.FC = () => {
|
|||
|
||||
return (
|
||||
<nav className="bg-white shadow-sm border-b border-gray-200">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center">
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ export const VideoClassificationProgress: React.FC<VideoClassificationProgressPr
|
|||
// 刷新队列状态的方法
|
||||
const refreshQueueStats = useCallback(async () => {
|
||||
if (projectId) {
|
||||
await getProjectQueueStatus(projectId);
|
||||
return await getProjectQueueStatus(projectId);
|
||||
} else {
|
||||
await refreshQueueStatus();
|
||||
return await refreshQueueStatus();
|
||||
}
|
||||
}, [projectId, getProjectQueueStatus, refreshQueueStatus]);
|
||||
|
||||
|
|
@ -132,21 +132,28 @@ export const VideoClassificationProgress: React.FC<VideoClassificationProgressPr
|
|||
// 队列控制
|
||||
const handlePauseResume = async () => {
|
||||
try {
|
||||
console.log('🎮 队列控制按钮点击,当前状态:', typedQueueStats?.status);
|
||||
|
||||
if (typedQueueStats?.status === 'Running') {
|
||||
console.log('⏸️ 暂停队列...');
|
||||
await pauseQueue();
|
||||
} else if (typedQueueStats?.status === 'Paused') {
|
||||
console.log('▶️ 恢复队列...');
|
||||
await resumeQueue();
|
||||
} else {
|
||||
console.log('❓ 未知状态,无法操作:', typedQueueStats?.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('队列控制失败:', error);
|
||||
console.error('❌ 队列控制失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStop = async () => {
|
||||
try {
|
||||
console.log('🛑 停止队列...');
|
||||
await stopQueue();
|
||||
} catch (error) {
|
||||
console.error('停止队列失败:', error);
|
||||
console.error('❌ 停止队列失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -208,11 +215,20 @@ export const VideoClassificationProgress: React.FC<VideoClassificationProgressPr
|
|||
{/* 队列控制按钮 */}
|
||||
{typedQueueStats && (
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* 暂停/恢复按钮 */}
|
||||
<button
|
||||
onClick={handlePauseResume}
|
||||
disabled={isLoading}
|
||||
className="p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-md transition-colors"
|
||||
title={typedQueueStats.status === 'Running' ? '暂停' : '恢复'}
|
||||
disabled={isLoading || (typedQueueStats.status !== 'Running' && typedQueueStats.status !== 'Paused')}
|
||||
className={`p-2 rounded-md transition-colors ${
|
||||
isLoading || (typedQueueStats.status !== 'Running' && typedQueueStats.status !== 'Paused')
|
||||
? 'text-gray-400 cursor-not-allowed'
|
||||
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
|
||||
}`}
|
||||
title={
|
||||
typedQueueStats.status === 'Running' ? '暂停队列' :
|
||||
typedQueueStats.status === 'Paused' ? '恢复队列' :
|
||||
'队列未运行'
|
||||
}
|
||||
>
|
||||
{typedQueueStats.status === 'Running' ? (
|
||||
<Pause className="w-4 h-4" />
|
||||
|
|
@ -220,15 +236,26 @@ export const VideoClassificationProgress: React.FC<VideoClassificationProgressPr
|
|||
<Play className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
|
||||
{/* 停止按钮 */}
|
||||
<button
|
||||
onClick={handleStop}
|
||||
disabled={isLoading}
|
||||
className="p-2 text-gray-600 hover:text-red-600 hover:bg-red-50 rounded-md transition-colors"
|
||||
title="停止"
|
||||
disabled={isLoading || typedQueueStats.status === 'Stopped'}
|
||||
className={`p-2 rounded-md transition-colors ${
|
||||
isLoading || typedQueueStats.status === 'Stopped'
|
||||
? 'text-gray-400 cursor-not-allowed'
|
||||
: 'text-gray-600 hover:text-red-600 hover:bg-red-50'
|
||||
}`}
|
||||
title={typedQueueStats.status === 'Stopped' ? '队列已停止' : '停止队列'}
|
||||
>
|
||||
<Square className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* 状态指示器 */}
|
||||
<div className="text-xs text-gray-500 ml-2">
|
||||
状态: {typedQueueStats.status}
|
||||
{isLoading && ' (操作中...)'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ interface VideoClassificationState {
|
|||
|
||||
// UI helpers
|
||||
clearError: () => void;
|
||||
refreshQueueStatus: () => Promise<void>;
|
||||
refreshQueueStatus: () => Promise<QueueStats | null>;
|
||||
refreshTaskProgress: () => Promise<void>;
|
||||
}
|
||||
|
||||
|
|
@ -185,10 +185,20 @@ export const useVideoClassificationStore = create<VideoClassificationState>((set
|
|||
stopQueue: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
console.log('🛑 调用停止队列命令...');
|
||||
await invoke('stop_classification_queue');
|
||||
await get().refreshQueueStatus();
|
||||
console.log('✅ 停止队列命令执行成功,刷新状态...');
|
||||
|
||||
// 等待一小段时间确保后端状态已更新
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// 刷新队列状态
|
||||
const newStats = await get().getQueueStatus();
|
||||
console.log('📊 刷新后的队列状态:', newStats);
|
||||
|
||||
set({ isLoading: false });
|
||||
} catch (error) {
|
||||
console.error('❌ 停止队列失败:', error);
|
||||
const errorMessage = typeof error === 'string' ? error : '停止队列失败';
|
||||
set({ error: errorMessage, isLoading: false });
|
||||
throw new Error(errorMessage);
|
||||
|
|
@ -198,10 +208,20 @@ export const useVideoClassificationStore = create<VideoClassificationState>((set
|
|||
pauseQueue: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
console.log('⏸️ 调用暂停队列命令...');
|
||||
await invoke('pause_classification_queue');
|
||||
await get().refreshQueueStatus();
|
||||
console.log('✅ 暂停队列命令执行成功,刷新状态...');
|
||||
|
||||
// 等待一小段时间确保后端状态已更新
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// 刷新队列状态
|
||||
const newStats = await get().getQueueStatus();
|
||||
console.log('📊 刷新后的队列状态:', newStats);
|
||||
|
||||
set({ isLoading: false });
|
||||
} catch (error) {
|
||||
console.error('❌ 暂停队列失败:', error);
|
||||
const errorMessage = typeof error === 'string' ? error : '暂停队列失败';
|
||||
set({ error: errorMessage, isLoading: false });
|
||||
throw new Error(errorMessage);
|
||||
|
|
@ -211,10 +231,20 @@ export const useVideoClassificationStore = create<VideoClassificationState>((set
|
|||
resumeQueue: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
console.log('▶️ 调用恢复队列命令...');
|
||||
await invoke('resume_classification_queue');
|
||||
await get().refreshQueueStatus();
|
||||
console.log('✅ 恢复队列命令执行成功,刷新状态...');
|
||||
|
||||
// 等待一小段时间确保后端状态已更新
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// 刷新队列状态
|
||||
const newStats = await get().getQueueStatus();
|
||||
console.log('📊 刷新后的队列状态:', newStats);
|
||||
|
||||
set({ isLoading: false });
|
||||
} catch (error) {
|
||||
console.error('❌ 恢复队列失败:', error);
|
||||
const errorMessage = typeof error === 'string' ? error : '恢复队列失败';
|
||||
set({ error: errorMessage, isLoading: false });
|
||||
throw new Error(errorMessage);
|
||||
|
|
@ -301,10 +331,12 @@ export const useVideoClassificationStore = create<VideoClassificationState>((set
|
|||
|
||||
refreshQueueStatus: async () => {
|
||||
try {
|
||||
await get().getQueueStatus();
|
||||
const stats = await get().getQueueStatus();
|
||||
return stats;
|
||||
} catch (error) {
|
||||
// 静默处理错误,避免重复设置错误状态
|
||||
console.error('刷新队列状态失败:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue