This commit is contained in:
truewhile
2026-09-04 11:56:29 +08:00
parent cc40169616
commit 203abd106a
72 changed files with 1979 additions and 1155 deletions
+9 -241
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import toast from 'react-hot-toast'
import {
AlertCircle,
@@ -8,12 +8,7 @@ import {
Copy,
Download,
Eye,
File,
FileText,
Film,
Image as ImageIcon,
Loader2,
MessageSquare,
PlayCircle,
RefreshCw,
Search,
@@ -26,6 +21,8 @@ import { strmAPI } from '../api/strm'
import type { StrmQueueSnapshot, StrmTask, StrmTaskStatus } from '../types/strm'
import { STRM_PROVIDER_LABELS } from '../types/strm'
import { apiErrorMessage, formatBytes, formatTime, taskStatusMeta } from './StrmManagePage'
import { TaskDetailModal, getFileIcon } from './strm-queue/TaskDetailModal'
import { copyToClipboard, useTaskSelection } from './queue-shared'
const FILTERS: { key: 'all' | StrmTaskStatus; label: string; icon: typeof Clock; color: string }[] = [
{ key: 'all', label: '全部', icon: Clock, color: 'text-ink-600' },
@@ -38,23 +35,6 @@ const FILTERS: { key: 'all' | StrmTaskStatus; label: string; icon: typeof Clock;
const PAGE_SIZE = 50
function getFileIcon(filename: string): ReactNode {
const ext = filename.split('.').pop()?.toLowerCase() ?? ''
if (['nfo', 'txt', 'xml', 'json'].includes(ext)) {
return <FileText size={15} className="text-amber-500 shrink-0" />
}
if (['jpg', 'jpeg', 'png', 'webp', 'bmp', 'gif', 'svg'].includes(ext)) {
return <ImageIcon size={15} className="text-blue-500 shrink-0" />
}
if (['srt', 'ass', 'ssa', 'sub', 'vtt'].includes(ext)) {
return <MessageSquare size={15} className="text-purple-500 shrink-0" />
}
if (['mkv', 'mp4', 'avi', 'mov', 'wmv', 'ts', 'flv', 'iso', 'm4v', 'strm'].includes(ext)) {
return <Film size={15} className="text-emerald-500 shrink-0" />
}
return <File size={15} className="text-gray-400 shrink-0" />
}
export function StrmQueuePanel({
kind,
embedded = false,
@@ -71,7 +51,7 @@ export function StrmQueuePanel({
const [isRefreshing, setIsRefreshing] = useState(false)
const [autoRefresh, setAutoRefresh] = useState(true)
const [batchBusy, setBatchBusy] = useState(false)
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
const { selectedIds, setSelectedIds, reset: clearSelection, toggleRow: toggleSelectRow, toggleAll: toggleAllIds } = useTaskSelection()
const [detailTask, setDetailTask] = useState<StrmTask | null>(null)
const isDownload = kind === 'download'
@@ -109,6 +89,7 @@ export function StrmQueuePanel({
useEffect(() => {
if (!autoRefresh) return
const timer = setInterval(() => {
if (document.hidden) return
refresh().catch(() => undefined)
}, 3000)
return () => clearInterval(timer)
@@ -116,13 +97,10 @@ export function StrmQueuePanel({
// Clear selections when changing filter or page
useEffect(() => {
setSelectedIds(new Set())
}, [filter, page])
clearSelection()
}, [filter, page, clearSelection])
const copyText = (text: string, label: string) => {
navigator.clipboard.writeText(text)
toast.success(`已复制${label}`)
}
const copyText = copyToClipboard
// Task actions
const cancelTask = async (task: StrmTask) => {
@@ -234,22 +212,7 @@ export function StrmQueuePanel({
const allCurrentChecked =
filteredTasks.length > 0 && filteredTasks.every((t) => selectedIds.has(t.id))
const toggleSelectAll = () => {
if (allCurrentChecked) {
setSelectedIds(new Set())
} else {
setSelectedIds(new Set(filteredTasks.map((t) => t.id)))
}
}
const toggleSelectRow = (id: string) => {
setSelectedIds((prev) => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
const toggleSelectAll = () => toggleAllIds(filteredTasks.map((t) => t.id))
return (
<div className="space-y-6">
@@ -996,201 +959,6 @@ export function StrmQueuePanel({
)
}
function TaskDetailModal({
task,
isDownload,
onClose,
onRetry,
onCancel,
onDelete,
onCopy,
}: {
task: StrmTask
isDownload: boolean
onClose: () => void
onRetry: (t: StrmTask) => void
onCancel: (t: StrmTask) => void
onDelete: (t: StrmTask) => void
onCopy: (text: string, label: string) => void
}) {
const status = taskStatusMeta(task.status)
return (
<div
className="fixed inset-0 z-[110] flex items-center justify-center bg-black/40 p-4 backdrop-blur-sm"
onClick={onClose}
>
<div
className="w-full max-w-xl rounded-3xl border border-gray-200 bg-white shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-150"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between border-b border-gray-100 px-6 py-4">
<div className="flex items-center gap-2">
{getFileIcon(task.file_name)}
<h3 className="font-display text-base font-bold text-ink-600">任务详情</h3>
</div>
<button
type="button"
onClick={onClose}
className="rounded-xl p-1 text-gray-400 hover:bg-gray-100 hover:text-ink-600 transition"
>
<X size={18} />
</button>
</div>
<div className="space-y-4 p-6 max-h-[70vh] overflow-y-auto text-xs">
{/* Main Info Box */}
<div className="rounded-2xl border border-gray-100 bg-gray-50/70 p-4 space-y-3">
<div className="flex items-center justify-between">
<span className="text-sand-500 font-medium">任务 ID</span>
<span className="font-mono text-ink-100 select-all">{task.id}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sand-500 font-medium">文件名称</span>
<span className="font-bold text-ink-600 select-all">{task.file_name}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sand-500 font-medium">文件大小</span>
<span className="font-mono text-ink-100">{formatBytes(task.size)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sand-500 font-medium">当前状态</span>
<span
className={`inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-semibold ${status.cls}`}
>
{status.label}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sand-500 font-medium">云盘提供方</span>
<span className="font-medium text-ink-100">
{STRM_PROVIDER_LABELS[task.provider] ?? task.provider}
</span>
</div>
{task.retry_count > 0 && (
<div className="flex items-center justify-between">
<span className="text-sand-500 font-medium">已重试次数</span>
<span className="font-bold text-amber-600">{task.retry_count} 次</span>
</div>
)}
</div>
{/* Paths */}
<div className="space-y-2">
<div className="flex items-center justify-between text-sand-500 font-medium">
<span>{isDownload ? '本地输出目标路径' : '本地来源路径'}</span>
<button
type="button"
onClick={() => onCopy(task.local_path, '本地路径')}
className="inline-flex items-center gap-1 text-brand-500 hover:underline"
>
<Copy size={11} /> 复制
</button>
</div>
<div className="rounded-xl border border-gray-200 bg-gray-50/50 p-3 font-mono text-[11px] text-ink-600 break-all select-all">
{task.local_path}
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-sand-500 font-medium">
<span>远端网盘路径</span>
<button
type="button"
onClick={() => onCopy(task.remote_path, '远端路径')}
className="inline-flex items-center gap-1 text-brand-500 hover:underline"
>
<Copy size={11} /> 复制
</button>
</div>
<div className="rounded-xl border border-gray-200 bg-gray-50/50 p-3 font-mono text-[11px] text-ink-600 break-all select-all">
{task.remote_path}
</div>
</div>
{/* Error Message Box */}
{task.error && (
<div className="space-y-2">
<div className="flex items-center justify-between text-rose-500 font-medium">
<span className="flex items-center gap-1">
<AlertCircle size={13} /> 错误详情
</span>
<button
type="button"
onClick={() => onCopy(task.error!, '错误信息')}
className="inline-flex items-center gap-1 text-rose-500 hover:underline"
>
<Copy size={11} /> 复制错误
</button>
</div>
<div className="rounded-xl border border-rose-200 bg-rose-50/60 p-3 font-mono text-[11px] text-rose-700 break-all select-all whitespace-pre-wrap">
{task.error}
</div>
</div>
)}
{/* Timeline */}
<div className="grid grid-cols-2 gap-3 pt-2 text-[11px] text-sand-500 border-t border-gray-100">
<div>创建时间:{formatTime(task.created_at)}</div>
{task.started_at && <div>开始时间:{formatTime(task.started_at)}</div>}
{task.finished_at && <div>结束时间:{formatTime(task.finished_at)}</div>}
</div>
</div>
{/* Footer Actions */}
<div className="flex items-center justify-between border-t border-gray-100 px-6 py-4 bg-gray-50/50">
<div>
{(task.status === 'done' ||
task.status === 'failed' ||
task.status === 'canceled') && (
<button
type="button"
onClick={() => onDelete(task)}
className="inline-flex items-center gap-1 rounded-xl border border-rose-200 bg-white px-3 py-2 text-xs font-semibold text-rose-500 hover:bg-rose-50 transition"
>
<Trash2 size={13} />
删除记录
</button>
)}
</div>
<div className="flex items-center gap-2">
{(task.status === 'pending' || task.status === 'running') && (
<button
type="button"
onClick={() => onCancel(task)}
className="inline-flex items-center gap-1 rounded-xl border border-amber-200 bg-white px-4 py-2 text-xs font-semibold text-amber-600 hover:bg-amber-50 transition"
>
<Ban size={13} />
取消任务
</button>
)}
{(task.status === 'failed' || task.status === 'canceled') && (
<button
type="button"
onClick={() => onRetry(task)}
className="neon-button !py-2 !px-4 text-xs font-semibold"
>
<RefreshCw size={13} />
重新入队
</button>
)}
<button
type="button"
onClick={onClose}
className="rounded-xl border border-gray-200 bg-white px-4 py-2 text-xs font-semibold text-ink-100 hover:bg-gray-50 transition"
>
关闭
</button>
</div>
</div>
</div>
</div>
)
}
export function StrmDownloadQueuePage() {
return <StrmQueuePanel kind="download" />
}