601 lines
18 KiB
TypeScript
601 lines
18 KiB
TypeScript
import { defineStore } from 'pinia';
|
||
import { ref, computed } from 'vue';
|
||
import downloadService from '@/services/download';
|
||
import type { DownloadTask } from '@/types';
|
||
|
||
export const useDownloadStore = defineStore('download', () => {
|
||
// 状态
|
||
const tasks = ref<DownloadTask[]>([]);
|
||
const loading = ref(false);
|
||
const error = ref<string | null>(null);
|
||
const lastUpdate = ref<number>(0);
|
||
|
||
// SSE连接管理
|
||
const sseConnections = ref<Map<string, () => void>>(new Map());
|
||
|
||
// 延迟更新管理
|
||
const delayedUpdates = ref<Map<string, number[]>>(new Map());
|
||
|
||
// 清理指定任务的所有延迟更新
|
||
const clearAllDelayedUpdates = (taskId: string) => {
|
||
const timeouts = delayedUpdates.value.get(taskId);
|
||
if (timeouts) {
|
||
timeouts.forEach(timeout => clearTimeout(timeout));
|
||
delayedUpdates.value.delete(taskId);
|
||
}
|
||
};
|
||
|
||
// 添加延迟更新
|
||
const addDelayedUpdate = (taskId: string, timeout: number) => {
|
||
if (!delayedUpdates.value.has(taskId)) {
|
||
delayedUpdates.value.set(taskId, []);
|
||
}
|
||
delayedUpdates.value.get(taskId)!.push(timeout);
|
||
};
|
||
|
||
// 计算属性:显示活跃任务和暂停任务
|
||
const activeTasks = computed(() => {
|
||
return tasks.value.filter(task =>
|
||
['downloading', 'paused'].includes(task.status)
|
||
);
|
||
});
|
||
|
||
// 计算属性:正在下载的任务
|
||
const downloadingTasks = computed(() => {
|
||
return tasks.value.filter(task => task.status === 'downloading');
|
||
});
|
||
|
||
// 计算属性:暂停的任务
|
||
const pausedTasks = computed(() => {
|
||
return tasks.value.filter(task => task.status === 'paused');
|
||
});
|
||
|
||
// 获取指定任务
|
||
const getTask = (taskId: string) => {
|
||
return tasks.value.find(task => task.id === taskId) || null;
|
||
};
|
||
|
||
// 获取指定作品的任务
|
||
const getArtworkTask = (artworkId: number) => {
|
||
return tasks.value.find(task =>
|
||
task.artwork_id === artworkId &&
|
||
['downloading', 'paused'].includes(task.status)
|
||
) || null;
|
||
};
|
||
|
||
// 获取任务列表(优化版本)
|
||
const fetchTasks = async () => {
|
||
try {
|
||
loading.value = true;
|
||
error.value = null;
|
||
|
||
// 使用增量更新API
|
||
const response = await downloadService.getTasksChanges(lastUpdate.value);
|
||
if (response.success) {
|
||
const { tasks: changedTasks, lastUpdate: newLastUpdate } = response.data;
|
||
|
||
// 更新任务列表
|
||
changedTasks.forEach((changedTask: DownloadTask) => {
|
||
const index = tasks.value.findIndex((t: DownloadTask) => t.id === changedTask.id);
|
||
if (index !== -1) {
|
||
tasks.value[index] = changedTask;
|
||
} else {
|
||
tasks.value.push(changedTask);
|
||
}
|
||
});
|
||
|
||
lastUpdate.value = newLastUpdate;
|
||
|
||
// 管理SSE连接
|
||
manageSSEConnections();
|
||
} else {
|
||
throw new Error(response.error || '获取任务列表失败');
|
||
}
|
||
} catch (err) {
|
||
error.value = err instanceof Error ? err.message : '获取任务列表失败';
|
||
console.error('获取任务列表失败:', err);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
// 获取活跃任务(轻量级)
|
||
const fetchActiveTasks = async () => {
|
||
try {
|
||
const response = await downloadService.getActiveTasks();
|
||
if (response.success) {
|
||
// 只更新活跃任务
|
||
const activeTaskIds = new Set(response.data.map((t: DownloadTask) => t.id));
|
||
|
||
// 移除已完成的活跃任务
|
||
tasks.value = tasks.value.filter((task: DownloadTask) =>
|
||
!activeTaskIds.has(task.id) || ['downloading', 'paused'].includes(task.status)
|
||
);
|
||
|
||
// 更新或添加活跃任务
|
||
response.data.forEach((activeTask: DownloadTask) => {
|
||
const index = tasks.value.findIndex((t: DownloadTask) => t.id === activeTask.id);
|
||
if (index !== -1) {
|
||
tasks.value[index] = activeTask;
|
||
} else {
|
||
tasks.value.push(activeTask);
|
||
}
|
||
});
|
||
|
||
// 管理SSE连接
|
||
manageSSEConnections();
|
||
}
|
||
} catch (err) {
|
||
console.error('获取活跃任务失败:', err);
|
||
}
|
||
};
|
||
|
||
// 获取任务摘要(用于快速状态检查)
|
||
const fetchTasksSummary = async () => {
|
||
try {
|
||
const response = await downloadService.getTasksSummary();
|
||
if (response.success) {
|
||
// 可以用于快速检查是否有新任务完成
|
||
return response.data;
|
||
}
|
||
} catch (err) {
|
||
console.error('获取任务摘要失败:', err);
|
||
}
|
||
return null;
|
||
};
|
||
|
||
// 开始SSE监听任务进度
|
||
const startTaskStreaming = (taskId: string) => {
|
||
// 如果已经有连接,先关闭
|
||
if (sseConnections.value.has(taskId)) {
|
||
sseConnections.value.get(taskId)!();
|
||
}
|
||
|
||
// console.log('开始SSE监听任务进度:', taskId);
|
||
|
||
// 添加超时处理 - 增加到60秒以匹配后端
|
||
const timeoutId = setTimeout(() => {
|
||
console.warn('SSE连接超时,关闭连接:', taskId);
|
||
stopTaskStreaming(taskId);
|
||
}, 60000); // 60秒超时
|
||
|
||
const closeConnection = downloadService.streamTaskProgress(
|
||
taskId,
|
||
(task) => {
|
||
// 验证task对象是否有效
|
||
if (!task || !task.id) {
|
||
console.error('收到无效的任务数据:', task);
|
||
return;
|
||
}
|
||
|
||
// console.log('收到SSE进度更新:', {
|
||
// taskId,
|
||
// status: task.status,
|
||
// progress: task.progress,
|
||
// completed: task.completed_files,
|
||
// total: task.total_files
|
||
// });
|
||
|
||
// 清除超时
|
||
clearTimeout(timeoutId);
|
||
|
||
// 更新任务状态
|
||
const index = tasks.value.findIndex(t => t.id === taskId);
|
||
if (index !== -1) {
|
||
// 保留临时状态(如pausing, resuming, cancelling)
|
||
const currentTask = tasks.value[index];
|
||
const isTemporaryStatus = ['pausing', 'resuming', 'cancelling'].includes(currentTask.status);
|
||
|
||
if (!isTemporaryStatus) {
|
||
tasks.value[index] = task;
|
||
} else {
|
||
// 只更新进度相关字段,保留临时状态
|
||
tasks.value[index] = {
|
||
...currentTask,
|
||
progress: task.progress,
|
||
completed_files: task.completed_files,
|
||
failed_files: task.failed_files,
|
||
recent_completed: task.recent_completed
|
||
};
|
||
}
|
||
} else {
|
||
// 如果是新任务,添加到列表
|
||
tasks.value.push(task);
|
||
}
|
||
|
||
// 如果任务完成或暂停,清理连接并触发额外的状态同步
|
||
if (['completed', 'failed', 'cancelled', 'partial', 'paused'].includes(task.status)) {
|
||
console.log('任务状态变更,关闭SSE连接:', taskId, task.status);
|
||
stopTaskStreaming(taskId);
|
||
|
||
// 如果任务完成,立即更新本地状态并停止所有延迟操作
|
||
if (['completed', 'failed', 'cancelled', 'partial'].includes(task.status)) {
|
||
console.log('任务完成,立即更新状态:', taskId, task.status);
|
||
|
||
// 立即更新本地任务状态,防止被其他操作覆盖
|
||
const index = tasks.value.findIndex(t => t.id === taskId);
|
||
if (index !== -1) {
|
||
tasks.value[index] = { ...task };
|
||
}
|
||
|
||
// 取消所有可能的延迟状态更新操作
|
||
clearAllDelayedUpdates(taskId);
|
||
}
|
||
}
|
||
},
|
||
() => {
|
||
console.log('SSE连接完成:', taskId);
|
||
clearTimeout(timeoutId);
|
||
stopTaskStreaming(taskId);
|
||
}
|
||
);
|
||
|
||
sseConnections.value.set(taskId, closeConnection);
|
||
};
|
||
|
||
// 停止SSE监听
|
||
const stopTaskStreaming = (taskId: string) => {
|
||
if (sseConnections.value.has(taskId)) {
|
||
sseConnections.value.get(taskId)!();
|
||
sseConnections.value.delete(taskId);
|
||
}
|
||
};
|
||
|
||
// 管理SSE连接
|
||
const manageSSEConnections = () => {
|
||
// 清理不需要的连接
|
||
const currentTaskIds = new Set(activeTasks.value.map(task => task.id));
|
||
|
||
// 关闭已不存在的任务的连接
|
||
sseConnections.value.forEach((closeConnection, taskId) => {
|
||
if (!currentTaskIds.has(taskId)) {
|
||
console.log('清理已不存在的任务连接:', taskId);
|
||
closeConnection();
|
||
sseConnections.value.delete(taskId);
|
||
}
|
||
});
|
||
|
||
// 为正在下载的任务建立连接,增加状态检查
|
||
activeTasks.value.forEach(task => {
|
||
if (task.status === 'downloading' && !sseConnections.value.has(task.id)) {
|
||
console.log('为下载任务建立SSE连接:', task.id, task.status);
|
||
startTaskStreaming(task.id);
|
||
}
|
||
});
|
||
|
||
// 清理已暂停或完成任务的连接
|
||
sseConnections.value.forEach((closeConnection, taskId) => {
|
||
const task = getTask(taskId);
|
||
if (task && ['paused', 'completed', 'failed', 'cancelled', 'partial'].includes(task.status)) {
|
||
console.log('清理非活跃任务的SSE连接:', taskId, task.status);
|
||
closeConnection();
|
||
sseConnections.value.delete(taskId);
|
||
}
|
||
});
|
||
};
|
||
|
||
// 清理所有SSE连接
|
||
const cleanupSSEConnections = () => {
|
||
sseConnections.value.forEach(closeConnection => {
|
||
closeConnection();
|
||
});
|
||
sseConnections.value.clear();
|
||
};
|
||
|
||
// 定期刷新任务列表
|
||
let refreshInterval: number | null = null;
|
||
let summaryInterval: number | null = null;
|
||
|
||
const startRefreshInterval = () => {
|
||
if (refreshInterval) return;
|
||
|
||
// 主要刷新:每5秒获取活跃任务(轻量级)
|
||
refreshInterval = window.setInterval(() => {
|
||
fetchActiveTasks();
|
||
}, 5000);
|
||
|
||
// 摘要检查:每30秒检查一次任务摘要,如果有变化则获取详细信息
|
||
summaryInterval = window.setInterval(async () => {
|
||
const summary = await fetchTasksSummary();
|
||
if (summary && summary.active > 0) {
|
||
// 如果有活跃任务,确保获取最新状态
|
||
fetchActiveTasks();
|
||
}
|
||
}, 30000);
|
||
};
|
||
|
||
const stopRefreshInterval = () => {
|
||
if (refreshInterval) {
|
||
clearInterval(refreshInterval);
|
||
refreshInterval = null;
|
||
}
|
||
if (summaryInterval) {
|
||
clearInterval(summaryInterval);
|
||
summaryInterval = null;
|
||
}
|
||
};
|
||
|
||
// 添加新任务(用于立即显示)
|
||
const addTask = (task: DownloadTask) => {
|
||
// 检查是否已存在
|
||
const existingIndex = tasks.value.findIndex(t => t.id === task.id);
|
||
if (existingIndex !== -1) {
|
||
tasks.value[existingIndex] = task;
|
||
} else {
|
||
tasks.value.push(task);
|
||
}
|
||
|
||
// 如果是下载中的任务,立即建立SSE连接
|
||
if (task.status === 'downloading') {
|
||
startTaskStreaming(task.id);
|
||
}
|
||
};
|
||
|
||
// 更新任务状态
|
||
const updateTask = (taskId: string, updates: Partial<DownloadTask>) => {
|
||
const index = tasks.value.findIndex(t => t.id === taskId);
|
||
if (index !== -1) {
|
||
tasks.value[index] = { ...tasks.value[index], ...updates };
|
||
}
|
||
};
|
||
|
||
// 移除任务
|
||
const removeTask = (taskId: string) => {
|
||
const index = tasks.value.findIndex(t => t.id === taskId);
|
||
if (index !== -1) {
|
||
tasks.value.splice(index, 1);
|
||
}
|
||
stopTaskStreaming(taskId);
|
||
};
|
||
|
||
// 取消任务
|
||
const cancelTask = async (taskId: string) => {
|
||
try {
|
||
// 立即停止SSE连接
|
||
stopTaskStreaming(taskId);
|
||
|
||
// 立即更新本地状态为取消中
|
||
updateTask(taskId, { status: 'cancelling' as any });
|
||
|
||
const response = await downloadService.cancelTask(taskId);
|
||
if (response.success) {
|
||
// 立即从任务列表中移除
|
||
removeTask(taskId);
|
||
// 异步刷新任务列表以确保同步
|
||
setTimeout(() => fetchTasks(), 500);
|
||
} else {
|
||
// 如果取消失败,恢复原状态
|
||
await fetchTasks();
|
||
throw new Error(response.error || '取消任务失败');
|
||
}
|
||
} catch (err) {
|
||
// 如果是网络错误或超时,提供更友好的错误信息
|
||
let errorMessage = '取消任务失败';
|
||
if (err instanceof Error) {
|
||
if (err.message.includes('timeout') || err.message.includes('network')) {
|
||
errorMessage = '网络连接超时,请检查网络连接后重试';
|
||
} else if (err.message.includes('404')) {
|
||
errorMessage = '任务不存在或已被删除';
|
||
} else {
|
||
errorMessage = err.message;
|
||
}
|
||
}
|
||
|
||
error.value = errorMessage;
|
||
console.error('取消任务失败:', err);
|
||
|
||
// 恢复任务状态
|
||
await fetchTasks();
|
||
throw new Error(errorMessage);
|
||
}
|
||
};
|
||
|
||
// 恢复任务
|
||
const resumeTask = async (taskId: string) => {
|
||
try {
|
||
// 获取任务信息以确定类型
|
||
const task = getTask(taskId);
|
||
|
||
// 立即更新本地状态为恢复中
|
||
updateTask(taskId, { status: 'resuming' as any });
|
||
|
||
let response;
|
||
|
||
// 判断是否为批量下载任务(batch、artist、art类型都是批量下载)
|
||
if (task && ['batch', 'artist', 'art'].includes(task.type)) {
|
||
// 使用批量下载专用API
|
||
response = await downloadService.resumeBatchTask(taskId);
|
||
} else {
|
||
// 使用单个下载API
|
||
response = await downloadService.resumeTask(taskId);
|
||
}
|
||
|
||
if (response.success) {
|
||
// 清理可能存在的延迟更新
|
||
clearAllDelayedUpdates(taskId);
|
||
|
||
// 使用后端返回的最新状态,确保状态同步
|
||
if (response.data) {
|
||
const index = tasks.value.findIndex(t => t.id === taskId);
|
||
if (index !== -1) {
|
||
tasks.value[index] = { ...response.data };
|
||
}
|
||
} else {
|
||
// 如果后端没有返回数据,则手动更新状态
|
||
updateTask(taskId, { status: 'downloading' });
|
||
}
|
||
|
||
// 立即建立SSE连接
|
||
startTaskStreaming(taskId);
|
||
} else {
|
||
// 如果恢复失败,恢复原状态
|
||
await fetchTasks();
|
||
throw new Error(response.error || '恢复任务失败');
|
||
}
|
||
} catch (err) {
|
||
// 提供更友好的错误信息
|
||
let errorMessage = '恢复任务失败';
|
||
if (err instanceof Error) {
|
||
if (err.message.includes('timeout') || err.message.includes('network')) {
|
||
errorMessage = '网络连接超时,请检查网络连接后重试';
|
||
} else if (err.message.includes('404')) {
|
||
errorMessage = '任务不存在或已被删除';
|
||
} else if (err.message.includes('already running')) {
|
||
errorMessage = '任务已在运行中';
|
||
} else {
|
||
errorMessage = err.message;
|
||
}
|
||
}
|
||
|
||
error.value = errorMessage;
|
||
console.error('恢复任务失败:', err);
|
||
|
||
// 恢复任务状态
|
||
await fetchTasks();
|
||
throw new Error(errorMessage);
|
||
}
|
||
};
|
||
|
||
// 暂停任务
|
||
const pauseTask = async (taskId: string) => {
|
||
try {
|
||
// 获取任务信息以确定类型
|
||
const task = getTask(taskId);
|
||
|
||
// 立即更新本地状态为暂停中
|
||
updateTask(taskId, { status: 'pausing' as any });
|
||
|
||
let response;
|
||
|
||
// 判断是否为批量下载任务(batch、artist、art类型都是批量下载)
|
||
if (task && ['batch', 'artist', 'art'].includes(task.type)) {
|
||
// 使用批量下载专用API
|
||
response = await downloadService.pauseBatchTask(taskId);
|
||
} else {
|
||
// 使用单个下载API
|
||
response = await downloadService.pauseTask(taskId);
|
||
}
|
||
|
||
if (response.success) {
|
||
// 清理可能存在的延迟更新
|
||
clearAllDelayedUpdates(taskId);
|
||
|
||
// 使用后端返回的最新状态,确保状态同步
|
||
if (response.data) {
|
||
const index = tasks.value.findIndex(t => t.id === taskId);
|
||
if (index !== -1) {
|
||
tasks.value[index] = { ...response.data };
|
||
}
|
||
} else {
|
||
// 如果后端没有返回数据,则手动更新状态
|
||
updateTask(taskId, { status: 'paused' });
|
||
}
|
||
|
||
// 停止SSE连接
|
||
stopTaskStreaming(taskId);
|
||
} else {
|
||
// 如果暂停失败,恢复原状态
|
||
await fetchTasks();
|
||
throw new Error(response.error || '暂停任务失败');
|
||
}
|
||
} catch (err) {
|
||
// 提供更友好的错误信息
|
||
let errorMessage = '暂停任务失败';
|
||
if (err instanceof Error) {
|
||
if (err.message.includes('timeout') || err.message.includes('network')) {
|
||
errorMessage = '网络连接超时,请检查网络连接后重试';
|
||
} else if (err.message.includes('404')) {
|
||
errorMessage = '任务不存在或已被删除';
|
||
} else if (err.message.includes('already paused')) {
|
||
errorMessage = '任务已暂停';
|
||
} else {
|
||
errorMessage = err.message;
|
||
}
|
||
}
|
||
|
||
error.value = errorMessage;
|
||
console.error('暂停任务失败:', err);
|
||
|
||
// 恢复任务状态
|
||
await fetchTasks();
|
||
throw new Error(errorMessage);
|
||
}
|
||
};
|
||
|
||
// 清理已完成的任务
|
||
const cleanupCompletedTasks = async (keepCount = 100) => {
|
||
try {
|
||
const response = await downloadService.cleanupTasks(true, keepCount);
|
||
if (response.success) {
|
||
await fetchTasks();
|
||
} else {
|
||
throw new Error(response.error || '清理任务失败');
|
||
}
|
||
} catch (err) {
|
||
error.value = err instanceof Error ? err.message : '清理任务失败';
|
||
console.error('清理任务失败:', err);
|
||
throw err;
|
||
}
|
||
};
|
||
|
||
// 清理历史记录
|
||
const cleanupHistory = async (keepCount = 500) => {
|
||
try {
|
||
const response = await downloadService.cleanupHistory(keepCount);
|
||
if (response.success) {
|
||
// 历史记录清理不影响当前任务状态
|
||
return response.data;
|
||
} else {
|
||
throw new Error(response.error || '清理历史失败');
|
||
}
|
||
} catch (err) {
|
||
error.value = err instanceof Error ? err.message : '清理历史失败';
|
||
console.error('清理历史失败:', err);
|
||
throw err;
|
||
}
|
||
};
|
||
|
||
// 清除错误
|
||
const clearError = () => {
|
||
error.value = null;
|
||
};
|
||
|
||
return {
|
||
// 状态
|
||
tasks,
|
||
loading,
|
||
error,
|
||
lastUpdate,
|
||
|
||
// 计算属性
|
||
activeTasks,
|
||
downloadingTasks,
|
||
pausedTasks,
|
||
|
||
// 方法
|
||
getTask,
|
||
getArtworkTask,
|
||
fetchTasks,
|
||
fetchActiveTasks,
|
||
fetchTasksSummary,
|
||
addTask,
|
||
updateTask,
|
||
removeTask,
|
||
cancelTask,
|
||
resumeTask,
|
||
pauseTask,
|
||
cleanupCompletedTasks,
|
||
cleanupHistory,
|
||
clearError,
|
||
|
||
// SSE管理
|
||
startTaskStreaming,
|
||
stopTaskStreaming,
|
||
manageSSEConnections,
|
||
cleanupSSEConnections,
|
||
|
||
// 定期刷新管理
|
||
startRefreshInterval,
|
||
stopRefreshInterval
|
||
};
|
||
}); |