性能优化
This commit is contained in:
@@ -9,10 +9,11 @@ import {
|
||||
} from '@lucide/vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { open as openDialog } from '@tauri-apps/plugin-dialog'
|
||||
// Rust 端通过 tauri-specta 生成的命令绑定(bindings.ts)
|
||||
import { commands } from '@/lib/bindings'
|
||||
import { useDownloaderStore, type DownloadTask, type TaskStatus, type CheckUrlResult } from '@/stores/downloaderStore'
|
||||
import { useModuleTabs } from '@/lib/useModuleTabs'
|
||||
import { useModuleTabs } from '@/lib/use-module-tabs'
|
||||
import { useModuleTabsStore } from '@/stores/moduleTabsStore'
|
||||
import { createLogger } from '@/lib/logger'
|
||||
import { pendingNewDownload } from '@/lib/trayEvents'
|
||||
@@ -218,13 +219,21 @@ const onRemoveOpenChange = (open: boolean) => {
|
||||
}
|
||||
|
||||
// ===== 任务详情弹窗 =====
|
||||
const detailDialogState = ref<{ open: boolean; task: DownloadTask | null }>({
|
||||
// 仅存任务 id,通过 computed 实时从 store.tasks 取最新对象,
|
||||
// 保证弹窗内的进度/速度/状态随下载进度事件实时刷新
|
||||
const detailDialogState = ref<{ open: boolean; taskId: string | null }>({
|
||||
open: false,
|
||||
task: null
|
||||
taskId: null
|
||||
})
|
||||
|
||||
const detailTask = computed<DownloadTask | null>(() => {
|
||||
const id = detailDialogState.value.taskId
|
||||
if (!id) return null
|
||||
return store.tasks.find((t) => t.id === id) ?? null
|
||||
})
|
||||
|
||||
const handleShowDetail = (task: DownloadTask) => {
|
||||
detailDialogState.value = { open: true, task }
|
||||
detailDialogState.value = { open: true, taskId: task.id }
|
||||
}
|
||||
|
||||
const handleCopyText = async (text: string, label: string) => {
|
||||
@@ -498,7 +507,7 @@ const handleDialogSave = async () => {
|
||||
const EXTENSION_STORE_URL = 'https://chromewebstore.google.com/'
|
||||
const handleInstallExtensionOnline = async () => {
|
||||
try {
|
||||
await invoke('downloader_open_url', { url: EXTENSION_STORE_URL })
|
||||
await commands.downloaderOpenUrl(EXTENSION_STORE_URL)
|
||||
} catch (e) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(EXTENSION_STORE_URL)
|
||||
@@ -537,6 +546,13 @@ onUnmounted(() => {
|
||||
|
||||
const allTasks = computed<DownloadTask[]>(() => store.tasks)
|
||||
|
||||
// 状态栏计数:单次遍历统计各状态任务数(替代模板内 4 次 filter 全量扫描)
|
||||
const statusCounts = computed(() => {
|
||||
const counts: Record<TaskStatus, number> = { queued: 0, active: 0, paused: 0, complete: 0, error: 0 }
|
||||
for (const t of allTasks.value) counts[t.status]++
|
||||
return counts
|
||||
})
|
||||
|
||||
// 状态筛选
|
||||
const filteredByStatus = computed<DownloadTask[]>(() => {
|
||||
if (statusFilter.value === 'all') return allTasks.value
|
||||
@@ -635,19 +651,19 @@ const toggleSortOrder = () => {
|
||||
<div v-if="running" class="flex items-center gap-2 text-xs">
|
||||
<Badge variant="secondary" class="gap-1">
|
||||
<Download class="h-3 w-3" />
|
||||
下载中 {{ allTasks.filter(t => t.status === 'active').length }}
|
||||
下载中 {{ statusCounts.active }}
|
||||
</Badge>
|
||||
<Badge variant="secondary" class="gap-1">
|
||||
<Clock class="h-3 w-3" />
|
||||
等待 {{ allTasks.filter(t => t.status === 'queued').length }}
|
||||
等待 {{ statusCounts.queued }}
|
||||
</Badge>
|
||||
<Badge v-if="allTasks.filter(t => t.status === 'paused').length > 0" variant="secondary" class="gap-1">
|
||||
<Badge v-if="statusCounts.paused > 0" variant="secondary" class="gap-1">
|
||||
<Pause class="h-3 w-3" />
|
||||
已暂停 {{ allTasks.filter(t => t.status === 'paused').length }}
|
||||
已暂停 {{ statusCounts.paused }}
|
||||
</Badge>
|
||||
<Badge variant="secondary" class="gap-1">
|
||||
<Check class="h-3 w-3" />
|
||||
已完成 {{ allTasks.filter(t => t.status === 'complete').length }}
|
||||
已完成 {{ statusCounts.complete }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1457,21 +1473,21 @@ const toggleSortOrder = () => {
|
||||
任务详情
|
||||
</DialogTitle>
|
||||
<DialogDescription class="text-xs">
|
||||
任务 ID:{{ detailDialogState.task?.id }}
|
||||
任务 ID:{{ detailTask?.id }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea class="max-h-[55vh] pr-3">
|
||||
<div v-if="detailDialogState.task" class="flex flex-col gap-3 py-2 text-sm">
|
||||
<div v-if="detailTask" class="flex flex-col gap-3 py-2 text-sm">
|
||||
<!-- 文件名 -->
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex flex-col gap-0.5 min-w-0 flex-1">
|
||||
<span class="text-xs text-muted-foreground">文件名</span>
|
||||
<span class="font-medium break-all">{{ detailDialogState.task.filename }}</span>
|
||||
<span class="font-medium break-all">{{ detailTask.filename }}</span>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button size="icon" variant="ghost" class="h-7 w-7 shrink-0" @click="handleCopyText(detailDialogState.task.filename, '文件名')">
|
||||
<Button size="icon" variant="ghost" class="h-7 w-7 shrink-0" @click="handleCopyText(detailTask.filename, '文件名')">
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -1484,9 +1500,9 @@ const toggleSortOrder = () => {
|
||||
<!-- 状态 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-muted-foreground">状态</span>
|
||||
<Badge :variant="getTaskStatusBadge(detailDialogState.task).variant" class="gap-1">
|
||||
<component :is="getTaskStatusBadge(detailDialogState.task).icon" class="h-3 w-3" />
|
||||
{{ getTaskStatusBadge(detailDialogState.task).text }}
|
||||
<Badge :variant="getTaskStatusBadge(detailTask).variant" class="gap-1">
|
||||
<component :is="getTaskStatusBadge(detailTask).icon" class="h-3 w-3" />
|
||||
{{ getTaskStatusBadge(detailTask).text }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -1494,11 +1510,11 @@ const toggleSortOrder = () => {
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex flex-col gap-0.5 min-w-0 flex-1">
|
||||
<span class="text-xs text-muted-foreground">下载链接</span>
|
||||
<span class="font-mono text-xs break-all">{{ detailDialogState.task.url }}</span>
|
||||
<span class="font-mono text-xs break-all">{{ detailTask.url }}</span>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button size="icon" variant="ghost" class="h-7 w-7 shrink-0" @click="handleCopyText(detailDialogState.task.url, '下载链接')">
|
||||
<Button size="icon" variant="ghost" class="h-7 w-7 shrink-0" @click="handleCopyText(detailTask.url, '下载链接')">
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -1514,15 +1530,15 @@ const toggleSortOrder = () => {
|
||||
<span class="text-xs text-muted-foreground">保存位置</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<span class="font-mono text-xs break-all cursor-default">{{ detailDialogState.task.dir }}</span>
|
||||
<span class="font-mono text-xs break-all cursor-default">{{ detailTask.dir }}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent class="max-w-[400px] break-all">{{ detailDialogState.task.dir }}</TooltipContent>
|
||||
<TooltipContent class="max-w-[400px] break-all">{{ detailTask.dir }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<span class="font-mono text-xs text-muted-foreground break-all">{{ detailDialogState.task.filename }}</span>
|
||||
<span class="font-mono text-xs text-muted-foreground break-all">{{ detailTask.filename }}</span>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button size="icon" variant="ghost" class="h-7 w-7 shrink-0" @click="handleCopyText(detailDialogState.task.dir + '\\' + detailDialogState.task.filename, '完整路径')">
|
||||
<Button size="icon" variant="ghost" class="h-7 w-7 shrink-0" @click="handleCopyText(detailTask.dir + '\\' + detailTask.filename, '完整路径')">
|
||||
<Copy class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -1536,19 +1552,19 @@ const toggleSortOrder = () => {
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-xs text-muted-foreground">文件总大小</span>
|
||||
<span>{{ formatSize(detailDialogState.task.totalSize) }}</span>
|
||||
<span>{{ formatSize(detailTask.totalSize) }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-xs text-muted-foreground">已下载</span>
|
||||
<span>{{ formatSize(detailDialogState.task.completedSize) }}</span>
|
||||
<span>{{ formatSize(detailTask.completedSize) }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-xs text-muted-foreground">下载进度</span>
|
||||
<span>{{ getProgress(detailDialogState.task) }}%</span>
|
||||
<span>{{ getProgress(detailTask) }}%</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-xs text-muted-foreground">当前速度</span>
|
||||
<span v-if="detailDialogState.task.status === 'active'">{{ formatSpeed(detailDialogState.task.speed) }}</span>
|
||||
<span v-if="detailTask.status === 'active'">{{ formatSpeed(detailTask.speed) }}</span>
|
||||
<span v-else>-</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1559,41 +1575,41 @@ const toggleSortOrder = () => {
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-xs text-muted-foreground">断点续传</span>
|
||||
<span>{{ detailDialogState.task.supportsResume ? '支持' : '不支持' }}</span>
|
||||
<span>{{ detailTask.supportsResume ? '支持' : '不支持' }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-xs text-muted-foreground">连接数 / 分片数</span>
|
||||
<span>{{ detailDialogState.task.segments.length }}</span>
|
||||
<span>{{ detailTask.segments.length }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-xs text-muted-foreground">创建时间</span>
|
||||
<span>{{ formatTime(detailDialogState.task.createdAt) }}</span>
|
||||
<span>{{ formatTime(detailTask.createdAt) }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-xs text-muted-foreground">剩余时间</span>
|
||||
<span v-if="detailDialogState.task.status === 'active' && detailDialogState.task.speed > 0">
|
||||
{{ formatEta(getEta(detailDialogState.task)) }}
|
||||
<span v-if="detailTask.status === 'active' && detailTask.speed > 0">
|
||||
{{ formatEta(getEta(detailTask)) }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<template v-if="detailDialogState.task.error">
|
||||
<template v-if="detailTask.error">
|
||||
<Separator />
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-xs text-muted-foreground">错误信息</span>
|
||||
<span class="text-sm text-destructive break-all">{{ detailDialogState.task.error }}</span>
|
||||
<span class="text-sm text-destructive break-all">{{ detailTask.error }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 自定义请求头 -->
|
||||
<template v-if="detailDialogState.task.headers && Object.keys(detailDialogState.task.headers).length > 0">
|
||||
<template v-if="detailTask.headers && Object.keys(detailTask.headers).length > 0">
|
||||
<Separator />
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-xs text-muted-foreground">自定义请求头</span>
|
||||
<div class="rounded-md bg-muted p-2 text-xs font-mono space-y-0.5">
|
||||
<div v-for="(value, key) in detailDialogState.task.headers" :key="key" class="flex gap-2">
|
||||
<div v-for="(value, key) in detailTask.headers" :key="key" class="flex gap-2">
|
||||
<span class="text-muted-foreground shrink-0">{{ key }}:</span>
|
||||
<span class="break-all">{{ value }}</span>
|
||||
</div>
|
||||
@@ -1602,12 +1618,12 @@ const toggleSortOrder = () => {
|
||||
</template>
|
||||
|
||||
<!-- 分段详情 -->
|
||||
<template v-if="detailDialogState.task.segments.length > 1">
|
||||
<template v-if="detailTask.segments.length > 1">
|
||||
<Separator />
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-xs text-muted-foreground">分段详情</span>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div v-for="(seg, i) in detailDialogState.task.segments" :key="i" class="flex items-center gap-2 text-xs">
|
||||
<div v-for="(seg, i) in detailTask.segments" :key="i" class="flex items-center gap-2 text-xs">
|
||||
<span class="w-8 text-muted-foreground shrink-0">#{{ i }}</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<Progress :model-value="segmentProgress(seg)" class="h-1.5" />
|
||||
@@ -1626,7 +1642,7 @@ const toggleSortOrder = () => {
|
||||
<DialogClose as-child>
|
||||
<Button variant="outline">关闭</Button>
|
||||
</DialogClose>
|
||||
<Button v-if="detailDialogState.task?.dir" variant="outline" @click="handleOpenDir(detailDialogState.task)">
|
||||
<Button v-if="detailTask?.dir" variant="outline" @click="handleOpenDir(detailTask)">
|
||||
<FolderOpen class="h-4 w-4" />
|
||||
打开目录
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user