下载模组更新,新增下载组件,下载监听改为全局,全量改为增量监听

This commit is contained in:
2025-08-31 06:41:46 +08:00
parent aa04f9d03f
commit ad5dfc64cb
17 changed files with 1662 additions and 285 deletions
+95 -6
View File
@@ -60,12 +60,30 @@
<div class="artworks-section">
<div class="section-header">
<h2>作品列表</h2>
<div class="artwork-filters">
<select v-model="artworkType" @change="handleTypeChange" class="filter-select">
<option value="art">插画</option>
<option value="manga"></option>
<option value="novel">小说</option>
</select>
<div class="header-controls">
<div class="artwork-filters">
<select v-model="artworkType" @change="handleTypeChange" class="filter-select">
<option value="art"></option>
<option value="manga">漫画</option>
<option value="novel">小说</option>
</select>
</div>
<!-- 顶部分页导航 -->
<div v-if="totalPages > 1 && artworks.length > 0" class="simple-pagination">
<button @click="goToPage(currentPage - 1)" class="simple-page-btn" :disabled="currentPage <= 1">
<svg viewBox="0 0 24 24" fill="currentColor" class="simple-page-icon">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" />
</svg>
</button>
<span class="simple-page-info">{{ currentPage }} / {{ totalPages }}</span>
<button @click="goToPage(currentPage + 1)" class="simple-page-btn"
:disabled="currentPage >= totalPages">
<svg viewBox="0 0 24 24" fill="currentColor" class="simple-page-icon">
<path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" />
</svg>
</button>
</div>
</div>
</div>
@@ -704,6 +722,7 @@ onMounted(async () => {
border-radius: 1rem;
padding: 2rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
position: relative;
}
.section-header {
@@ -720,6 +739,12 @@ onMounted(async () => {
margin: 0;
}
.header-controls {
display: flex;
align-items: center;
gap: 1rem;
}
.artwork-filters {
display: flex;
gap: 1rem;
@@ -734,6 +759,58 @@ onMounted(async () => {
color: #374151;
}
/* 顶部分页导航样式 */
.simple-pagination {
display: flex;
align-items: center;
gap: 0.5rem;
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.simple-page-btn {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
background: white;
color: #374151;
cursor: pointer;
transition: all 0.2s;
padding: 0;
}
.simple-page-btn:hover:not(:disabled) {
background: #f3f4f6;
border-color: #9ca3af;
transform: translateY(-1px);
}
.simple-page-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none;
}
.simple-page-icon {
width: 1.25rem;
height: 1.25rem;
}
.simple-page-info {
font-size: 0.875rem;
color: #6b7280;
font-weight: 500;
min-width: 3rem;
text-align: center;
}
.artworks-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
@@ -901,6 +978,12 @@ onMounted(async () => {
align-items: stretch;
}
/* 移动端简洁分页导航样式调整 */
.simple-pagination {
justify-content: center;
margin-bottom: 1rem;
}
.artist-profile {
flex-direction: column;
text-align: center;
@@ -930,6 +1013,12 @@ onMounted(async () => {
align-items: stretch;
}
.header-controls {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.artworks-grid {
grid-template-columns: 1fr;
}
+41 -115
View File
@@ -25,7 +25,7 @@
<ArtworkInfoPanel :artwork="artwork" :downloading="downloading" :is-downloaded="isDownloaded"
:current-task="currentTask" :loading="loading" :show-navigation="showNavigation"
:previous-artwork="previousArtwork" :next-artwork="nextArtwork" :selected-tags="selectedTags"
@download="handleDownload" @bookmark="handleBookmark" @update-task="updateTask" @remove-task="removeTask"
@download="handleDownload" @bookmark="handleBookmark"
@go-back="goBackToArtist" @navigate-previous="navigateToPrevious" @navigate-next="navigateToNext"
@tag-click="handleTagClick" />
</div>
@@ -38,6 +38,7 @@ import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useAuthStore } from '@/stores/auth';
import { useRepositoryStore } from '@/stores/repository';
import { useDownloadStore } from '@/stores/download';
import artworkService from '@/services/artwork';
import artistService from '@/services/artist';
import downloadService from '@/services/download';
@@ -53,6 +54,7 @@ const route = useRoute();
const router = useRouter();
const authStore = useAuthStore();
const repositoryStore = useRepositoryStore();
const downloadStore = useDownloadStore();
// 状态
const artwork = ref<Artwork | null>(null);
@@ -62,9 +64,11 @@ const currentPage = ref(0);
const downloading = ref(false);
const isDownloaded = ref(false);
// 下载任务状态
const currentTask = ref<DownloadTask | null>(null);
const sseConnection = ref<(() => void) | null>(null);
// 下载任务状态 - 使用Pinia store
const currentTask = computed(() => {
if (!artwork.value) return null;
return downloadStore.getArtworkTask(artwork.value.id);
});
// 收藏错误状态
const bookmarkError = ref<string | null>(null);
@@ -105,10 +109,8 @@ const fetchArtworkDetail = async () => {
loading.value = true;
error.value = null;
// 立即清理所有下载相关状态
currentTask.value = null;
// 清理下载状态
downloading.value = false;
stopTaskStreaming();
const response = await artworkService.getArtworkDetail(artworkId);
@@ -137,19 +139,19 @@ const checkDownloadStatus = async (artworkId: number, retryCount = 0) => {
try {
const response = await repositoryStore.checkArtworkDownloaded(artworkId);
console.log('下载状态检查响应:', response);
// console.log('下载状态检查响应:', response);
// repository store的apiCall返回的是data.data,所以response直接是数据对象
if (response && typeof response === 'object') {
const newStatus = response.is_downloaded || false;
// 如果状态发生变化,记录日志
if (isDownloaded.value !== newStatus) {
console.log(`作品下载状态变化: ${isDownloaded.value} -> ${newStatus}`);
}
// if (isDownloaded.value !== newStatus) {
// console.log(`作品下载状态变化: ${isDownloaded.value} -> ${newStatus}`);
// }
isDownloaded.value = newStatus;
console.log('作品下载状态:', isDownloaded.value);
// console.log('作品下载状态:', isDownloaded.value);
}
} catch (err) {
console.error('检查下载状态失败:', err);
@@ -170,8 +172,7 @@ const handleDownload = async () => {
if (!artwork.value) return;
try {
// 清理之前的任务状态
currentTask.value = null;
// 清理下载状态
downloading.value = true;
// 如果已经下载过,则强制重新下载(跳过现有文件检查)
@@ -191,10 +192,10 @@ const handleDownload = async () => {
return;
}
// 如果是新任务,立即创建任务状态并开始监听进度
// 如果是新任务,立即添加到store
if (response.data.task_id) {
// 立即创建任务状态,让进度条立即显示
currentTask.value = {
const newTask: DownloadTask = {
id: response.data.task_id,
type: 'artwork',
status: 'downloading',
@@ -208,8 +209,8 @@ const handleDownload = async () => {
start_time: new Date().toISOString()
};
// 立即开始SSE监听任务进度
startTaskStreaming(response.data.task_id);
// 添加到storestore会自动管理SSE连接
downloadStore.addTask(newTask);
}
} else {
throw new Error(response.error || '下载失败');
@@ -222,93 +223,25 @@ const handleDownload = async () => {
}
};
// 开始SSE监听任务进度
const startTaskStreaming = (taskId: string) => {
// 清除之前的连接
if (sseConnection.value) {
sseConnection.value();
}
console.log('开始SSE监听任务进度:', taskId);
// 建立SSE连接
sseConnection.value = downloadService.streamTaskProgress(
taskId,
(task) => {
console.log('收到SSE进度更新:', {
taskId,
status: task.status,
progress: task.progress,
completed: task.completed_files,
total: task.total_files
});
// 立即更新任务状态,让进度条立即显示
currentTask.value = task;
// 如果任务完成,清理连接并检查下载状态
if (['completed', 'failed', 'cancelled', 'partial'].includes(task.status)) {
console.log('任务完成,关闭SSE连接');
stopTaskStreaming();
// 延迟检查下载状态,确保文件写入完成
// 减少延迟时间,提高响应速度
const delay = task.total_files > 1 ? 1500 : 1000; // 多文件延迟1.5秒,单文件延迟1秒
setTimeout(async () => {
// 检查当前页面是否还是同一个作品,避免页面切换后的状态更新
if (artwork.value && artwork.value.id === task.artwork_id) {
console.log(`延迟 ${delay}ms 后检查下载状态`);
await checkDownloadStatus(artwork.value.id);
// 如果任务完成但状态检查显示未下载,再次延迟检查
if (task.status === 'completed' && !isDownloaded.value) {
console.log('任务完成但状态检查失败,再次延迟检查');
setTimeout(async () => {
if (artwork.value && artwork.value.id === task.artwork_id) {
await checkDownloadStatus(artwork.value.id);
}
}, 1000);
}
// 清理任务状态,显示下载完成状态
currentTask.value = null;
}
}, delay);
}
},
() => {
console.log('SSE连接完成');
stopTaskStreaming();
// 监听任务完成状态
watch(currentTask, (newTask, oldTask) => {
if (oldTask && !newTask) {
// 任务被移除,检查下载状态
if (artwork.value) {
setTimeout(async () => {
await checkDownloadStatus(artwork.value!.id);
}, 1000);
}
} else if (newTask && ['completed', 'failed', 'cancelled', 'partial'].includes(newTask.status)) {
// 任务完成,延迟检查下载状态
if (artwork.value && artwork.value.id === newTask.artwork_id) {
const delay = newTask.total_files > 1 ? 1500 : 1000;
setTimeout(async () => {
await checkDownloadStatus(artwork.value!.id);
}, delay);
}
);
};
// 停止SSE监听
const stopTaskStreaming = () => {
if (sseConnection.value) {
sseConnection.value();
sseConnection.value = null;
}
// 确保清理任务状态
currentTask.value = null;
downloading.value = false;
};
// 更新任务状态
const updateTask = (task: DownloadTask) => {
currentTask.value = task;
};
// 移除任务
const removeTask = (taskId: string) => {
if (currentTask.value?.id === taskId) {
currentTask.value = null;
stopTaskStreaming();
}
};
}, { immediate: true });
// 收藏/取消收藏
const handleBookmark = async () => {
@@ -386,10 +319,8 @@ const fetchArtistArtworks = async () => {
// 导航到上一个作品
const navigateToPrevious = () => {
if (previousArtwork.value && !loading.value) {
// 立即清理下载任务状态
currentTask.value = null;
// 清理下载状态
downloading.value = false;
stopTaskStreaming();
// 立即设置加载状态
loading.value = true;
@@ -409,10 +340,8 @@ const navigateToPrevious = () => {
// 导航到下一个作品
const navigateToNext = () => {
if (nextArtwork.value && !loading.value) {
// 立即清理下载任务状态
currentTask.value = null;
// 清理下载状态
downloading.value = false;
stopTaskStreaming();
// 立即设置加载状态
loading.value = true;
@@ -538,10 +467,8 @@ watch(() => route.params.id, (newId, oldId) => {
// 确保页面滚动到顶部
window.scrollTo(0, 0);
// 立即清理所有下载相关状态
currentTask.value = null;
downloading.value = false;
stopTaskStreaming();
// 清理下载状态
downloading.value = false;
// 重新获取作品详情
fetchArtworkDetail();
@@ -583,12 +510,11 @@ onMounted(() => {
document.addEventListener('keyup', handleKeyUp);
});
// 组件卸载时移除事件监听和清理SSE连接
// 组件卸载时移除事件监听
onUnmounted(() => {
document.removeEventListener('keydown', handleKeydown);
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('keyup', handleKeyUp);
stopTaskStreaming();
});
</script>
+27 -156
View File
@@ -197,22 +197,23 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import { useAuthStore } from '@/stores/auth';
import { useDownloadStore } from '@/stores/download';
import downloadService from '@/services/download';
import type { DownloadTask } from '@/types';
const authStore = useAuthStore();
const downloadStore = useDownloadStore();
// 状态
const activeTab = ref<'tasks' | 'history'>('tasks');
const loading = ref(false);
const error = ref<string | null>(null);
const tasks = ref<DownloadTask[]>([]);
const history = ref<any[]>([]);
// SSE连接管理
const sseConnections = ref<Map<string, () => void>>(new Map());
// 使用store中的任务数据
const tasks = computed(() => downloadStore.tasks);
// 计算属性:显示活跃任务和暂停任务
const activeTasks = computed(() => {
@@ -295,32 +296,11 @@ const formatDate = (dateString: string) => {
// 刷新数据
const refreshData = async () => {
await Promise.all([
fetchTasks(),
downloadStore.fetchTasks(),
fetchHistory()
]);
};
// 获取任务列表
const fetchTasks = async () => {
try {
const response = await downloadService.getAllTasks();
if (response.success) {
tasks.value = response.data || [];
// 只为正在下载的任务建立SSE连接,避免为暂停任务建立连接
activeTasks.value.forEach(task => {
if (task.status === 'downloading' && !sseConnections.value.has(task.id)) {
startTaskStreaming(task.id);
}
});
} else {
throw new Error(response.error || '获取任务列表失败');
}
} catch (err) {
console.error('获取任务列表失败:', err);
}
};
// 获取历史记录(只获取最近200条)
const fetchHistory = async () => {
try {
@@ -337,105 +317,25 @@ const fetchHistory = async () => {
}
};
// 开始SSE监听任务进度
const startTaskStreaming = (taskId: string) => {
// 如果已经有连接,先关闭
if (sseConnections.value.has(taskId)) {
sseConnections.value.get(taskId)!();
}
console.log('开始SSE监听任务进度:', taskId);
// 添加超时处理
const timeoutId = setTimeout(() => {
console.warn('SSE连接超时,关闭连接:', taskId);
stopTaskStreaming(taskId);
}, 30000); // 30秒超时
const closeConnection = downloadService.streamTaskProgress(
taskId,
(task) => {
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) {
tasks.value[index] = task;
}
// 如果任务完成或暂停,清理连接
if (['completed', 'failed', 'cancelled', 'partial', 'paused'].includes(task.status)) {
console.log('任务状态变更,关闭SSE连接:', taskId);
stopTaskStreaming(taskId);
// 延迟刷新历史记录
if (['completed', 'failed', 'cancelled', 'partial'].includes(task.status)) {
setTimeout(() => {
fetchHistory();
}, 1000);
}
}
},
() => {
console.log('SSE连接完成:', taskId);
clearTimeout(timeoutId);
stopTaskStreaming(taskId);
}
// 监听任务完成,刷新历史记录
watch(tasks, (newTasks, oldTasks) => {
// 检查是否有任务完成
const completedTasks = newTasks.filter(task =>
['completed', 'failed', 'cancelled', 'partial'].includes(task.status)
);
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)) {
startTaskStreaming(task.id);
}
});
};
if (completedTasks.length > 0) {
// 延迟刷新历史记录
setTimeout(() => {
fetchHistory();
}, 1000);
}
}, { deep: true });
// 取消任务
const cancelTask = async (taskId: string) => {
try {
const response = await downloadService.cancelTask(taskId);
if (response.success) {
// 立即停止SSE连接
stopTaskStreaming(taskId);
await fetchTasks();
} else {
throw new Error(response.error || '取消任务失败');
}
await downloadStore.cancelTask(taskId);
} catch (err) {
error.value = err instanceof Error ? err.message : '取消任务失败';
console.error('取消任务失败:', err);
@@ -445,14 +345,7 @@ const cancelTask = async (taskId: string) => {
// 恢复任务
const resumeTask = async (taskId: string) => {
try {
const response = await downloadService.resumeTask(taskId);
if (response.success) {
await fetchTasks();
// 重新管理SSE连接
manageSSEConnections();
} else {
throw new Error(response.error || '恢复任务失败');
}
await downloadStore.resumeTask(taskId);
} catch (err) {
error.value = err instanceof Error ? err.message : '恢复任务失败';
console.error('恢复任务失败:', err);
@@ -464,13 +357,9 @@ const cleanupHistory = async () => {
if (confirm('确定要清理下载历史吗?这将保留最新的500条记录。')) {
try {
loading.value = true;
const response = await downloadService.cleanupHistory(500);
if (response.success) {
await fetchHistory(); // 重新获取历史记录
alert('下载历史已清理!');
} else {
throw new Error(response.error || '清理历史失败');
}
await downloadStore.cleanupHistory(500);
await fetchHistory(); // 重新获取历史记录
alert('下载历史已清理!');
} catch (err) {
error.value = err instanceof Error ? err.message : '清理历史失败';
console.error('清理历史失败:', err);
@@ -485,13 +374,8 @@ const cleanupTasks = async () => {
if (confirm('确定要清理已完成的任务吗?这将保留活跃任务和最新的100个已完成任务。')) {
try {
loading.value = true;
const response = await downloadService.cleanupTasks(true, 100);
if (response.success) {
await fetchTasks(); // 重新获取任务列表
alert('下载任务已清理!');
} else {
throw new Error(response.error || '清理任务失败');
}
await downloadStore.cleanupCompletedTasks(100);
alert('下载任务已清理!');
} catch (err) {
error.value = err instanceof Error ? err.message : '清理任务失败';
console.error('清理任务失败:', err);
@@ -506,27 +390,14 @@ const clearError = () => {
error.value = null;
};
// 清理所有SSE连接
const cleanupSSEConnections = () => {
sseConnections.value.forEach(closeConnection => {
closeConnection();
});
sseConnections.value.clear();
};
onMounted(async () => {
loading.value = true;
try {
// 先获取数据,不阻塞页面渲染
await Promise.all([
fetchTasks(),
downloadStore.fetchTasks(),
fetchHistory()
]);
// 数据加载完成后,异步管理SSE连接
setTimeout(() => {
manageSSEConnections();
}, 100);
} catch (err) {
error.value = err instanceof Error ? err.message : '加载数据失败';
} finally {
@@ -535,7 +406,7 @@ onMounted(async () => {
});
onUnmounted(() => {
cleanupSSEConnections();
// 组件卸载时不需要清理SSE连接,因为store会统一管理
});
</script>