This commit is contained in:
zhongluofeng
2026-07-30 09:09:29 +08:00
parent 74902c4cec
commit f452322aad
32 changed files with 4586 additions and 115 deletions
+220 -21
View File
@@ -11,9 +11,10 @@ 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'
import { useDownloaderStore, type DownloadTask, type TaskStatus } from '@/stores/downloaderStore'
import { useDownloaderStore, type DownloadTask, type TaskStatus, type CheckUrlResult } from '@/stores/downloaderStore'
import { useModuleTabs } from '@/lib/useModuleTabs'
import { createLogger } from '@/lib/logger'
import { pendingNewDownload } from '@/lib/trayEvents'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
@@ -257,6 +258,15 @@ const handleOpenDir = async (task: DownloadTask) => {
}
// ===== 添加下载 =====
/** 重复确认对话框状态 */
const duplicateDialogState = ref<{
open: boolean
url: string
result: CheckUrlResult | null
pendingUrls: string[]
currentIndex: number
}>({ open: false, url: '', result: null, pendingUrls: [], currentIndex: 0 })
const handleAddDownload = async () => {
const text = addUriText.value.trim()
if (!text) {
@@ -272,23 +282,23 @@ const handleAddDownload = async () => {
addingTask.value = true
try {
const dir = addDir.value.trim() || undefined
let successCount = 0
for (const uri of uris) {
try {
await store.addTask(uri, undefined, dir)
successCount++
} catch (e) {
logger.error(`添加 ${uri} 失败: ` + e)
}
}
if (successCount > 0) {
toast.success(`已添加 ${successCount} 个下载任务`)
addUriText.value = ''
addDir.value = ''
addDialogOpen.value = false
activeTab.value = 'tasks'
// 检查是否启用重复检查
const checkEnabled = store.settings?.checkDuplicate ?? true
if (checkEnabled) {
// 逐个检查重复
await processUrlsWithCheck(uris, dir)
} else {
toast.error('添加任务失败')
// 直接添加
let successCount = 0
for (const uri of uris) {
try {
await store.addTask(uri, undefined, dir, undefined, true)
successCount++
} catch (e) {
logger.error(`添加 ${uri} 失败: ` + e)
}
}
finishAdd(successCount)
}
} catch (e) {
toast.error('添加失败: ' + e)
@@ -297,6 +307,146 @@ const handleAddDownload = async () => {
}
}
/** 逐个检查 URL 重复性,发现重复时弹出确认对话框 */
async function processUrlsWithCheck(urls: string[], dir: string | undefined) {
let successCount = 0
for (let i = 0; i < urls.length; i++) {
const uri = urls[i]
try {
const result = await store.checkUrl(uri, dir)
if (!result.ok) {
// 探测失败:直接添加(后端会标记为 error)
try {
await store.addTask(uri, undefined, dir, undefined, true)
successCount++
} catch (e) {
logger.error(`添加 ${uri} 失败: ` + e)
}
continue
}
if (result.duplicate === 'none') {
// 无重复:直接添加(自动重命名以防磁盘文件冲突)
try {
await store.addTask(uri, undefined, dir, undefined, true)
successCount++
} catch (e) {
logger.error(`添加 ${uri} 失败: ` + e)
}
continue
}
// 发现重复:弹出确认对话框,暂停处理
duplicateDialogState.value = {
open: true,
url: uri,
result,
pendingUrls: urls,
currentIndex: i,
}
return // 等待用户确认后继续
} catch (e) {
logger.error(`检查 ${uri} 失败: ` + e)
// 检查失败:直接添加
try {
await store.addTask(uri, undefined, dir, undefined, true)
successCount++
} catch (e2) {
logger.error(`添加 ${uri} 失败: ` + e2)
}
}
}
finishAdd(successCount)
}
/** 完成添加:显示提示并关闭对话框 */
function finishAdd(successCount: number) {
if (successCount > 0) {
toast.success(`已添加 ${successCount} 个下载任务`)
addUriText.value = ''
addDir.value = ''
addDialogOpen.value = false
activeTab.value = 'tasks'
} else {
toast.error('添加任务失败')
}
}
/** 重复确认对话框:用户选择"仍然下载"(自动重命名) */
const onDuplicateConfirm = async () => {
const { url, pendingUrls, currentIndex } = duplicateDialogState.value
const dir = addDir.value.trim() || undefined
duplicateDialogState.value.open = false
let successCount = 0
try {
await store.addTask(url, undefined, dir, undefined, true) // autoRename=true
successCount++
} catch (e) {
logger.error(`添加 ${url} 失败: ` + e)
}
// 继续处理剩余 URL
const remaining = pendingUrls.slice(currentIndex + 1)
for (const uri of remaining) {
try {
const result = await store.checkUrl(uri, dir)
if (result.ok && result.duplicate !== 'none') {
// 又发现重复,再次弹出确认
duplicateDialogState.value = {
open: true,
url: uri,
result,
pendingUrls,
currentIndex: pendingUrls.indexOf(uri),
}
// 已添加的成功数通过闭包传递不太方便,直接在这里 finish 后再继续
// 简化处理:保存 successCount 到 state,下一次确认时累加
duplicateSuccessCount.value = successCount
return
}
await store.addTask(uri, undefined, dir, undefined, true)
successCount++
} catch (e) {
logger.error(`添加 ${uri} 失败: ` + e)
}
}
finishAdd(successCount)
}
/** 累积的成功计数(跨多次重复确认) */
const duplicateSuccessCount = ref(0)
/** 重复确认对话框:用户选择"跳过" */
const onDuplicateSkip = () => {
const { pendingUrls, currentIndex } = duplicateDialogState.value
duplicateDialogState.value.open = false
const remaining = pendingUrls.slice(currentIndex + 1)
if (remaining.length > 0) {
// 继续处理剩余 URL(不添加当前这个)
const dir = addDir.value.trim() || undefined
processUrlsWithCheck(remaining, dir).then(() => {})
} else {
finishAdd(duplicateSuccessCount.value)
duplicateSuccessCount.value = 0
}
}
/** 重复确认对话框:用户取消整个添加流程 */
const onDuplicateCancel = () => {
duplicateDialogState.value.open = false
finishAdd(duplicateSuccessCount.value)
duplicateSuccessCount.value = 0
}
/** 重复类型文本 */
const duplicateKindText = (kind: string): string => {
switch (kind) {
case 'url': return 'URL 已存在下载任务'
case 'filename': return '同名文件已在下载列表中'
case 'fileExists': return '目标目录已存在同名文件'
default: return '重复'
}
}
const handleSelectDir = async () => {
try {
const selected = await openDialog({ directory: true, multiple: false })
@@ -367,6 +517,11 @@ const handleCopy = async (text: string, label: string) => {
onMounted(async () => {
await store.init()
// 消费托盘菜单"新建下载"标志位
if (pendingNewDownload.value) {
pendingNewDownload.value = false
addDialogOpen.value = true
}
})
onUnmounted(() => {
@@ -458,8 +613,8 @@ const toggleSortOrder = () => {
<TabsContent value="tasks" class="flex-1 mt-4 min-h-0 tab-animate">
<div class="h-full flex flex-col gap-4">
<!-- 状态栏 -->
<Card class="shrink-0">
<CardContent class="pl-4 pr-4">
<Card class="shrink-0 !py-0 !gap-0">
<CardContent class="pl-4 pr-4 py-3">
<div class="flex items-center justify-between flex-wrap gap-3">
<div class="flex items-center gap-3">
<div class="flex items-center gap-2">
@@ -611,8 +766,8 @@ const toggleSortOrder = () => {
</Button>
</div>
<div v-else key="task-list" class="flex flex-col gap-3 pb-4">
<Card v-for="task in pagedTasks" :key="task.id" class="overflow-hidden">
<CardContent class="pl-4 pr-4">
<Card v-for="task in pagedTasks" :key="task.id" class="overflow-hidden !py-0 !gap-0">
<CardContent class="pl-4 pr-4 py-3">
<div class="flex items-start justify-between gap-3 mb-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
@@ -893,6 +1048,17 @@ const toggleSortOrder = () => {
@update:model-value="(v: boolean) => store.settings && (store.settings.deleteFilesOnRemove = v)"
/>
</div>
<div class="flex items-center justify-between">
<div class="flex flex-col gap-0.5">
<Label for="check-duplicate" class="cursor-pointer">重复下载检查</Label>
<span class="text-xs text-muted-foreground">添加下载前检查 URL 和文件名重复,发现重复时询问</span>
</div>
<Switch
id="check-duplicate"
:model-value="store.settings?.checkDuplicate ?? true"
@update:model-value="(v: boolean) => store.settings && (store.settings.checkDuplicate = v)"
/>
</div>
</CardContent>
</Card>
@@ -1218,6 +1384,39 @@ const toggleSortOrder = () => {
</DialogContent>
</Dialog>
<!-- ===== 重复下载确认弹窗 ===== -->
<AlertDialog :open="duplicateDialogState.open">
<AlertDialogContent class="max-w-md">
<AlertDialogHeader>
<AlertDialogTitle class="flex items-center gap-2">
<AlertCircle class="h-4 w-4 text-amber-500" />
下载重复提醒
</AlertDialogTitle>
<AlertDialogDescription class="text-xs">
<span class="block mb-2">
{{ duplicateKindText(duplicateDialogState.result?.duplicate ?? '') }}
</span>
<span class="block font-mono text-xs break-all">{{ duplicateDialogState.url }}</span>
<span v-if="duplicateDialogState.result?.existing" class="block mt-2 text-xs">
已存在
<span class="font-medium">{{ duplicateDialogState.result.existing.filename }}</span>
</span>
<span class="block mt-2 text-muted-foreground">
选择"仍然下载"将自动重命名追加序号选择"跳过"将不下载此链接
</span>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel @click="onDuplicateCancel">取消全部</AlertDialogCancel>
<Button variant="outline" @click="onDuplicateSkip">跳过此链接</Button>
<AlertDialogAction @click="onDuplicateConfirm">
<Download class="h-4 w-4" />
仍然下载
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- ===== 删除任务专用对话框"同时删除文件"开关 ===== -->
<AlertDialog :open="removeDialogState.open" @update:open="onRemoveOpenChange">
<AlertDialogContent>