mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-04 20:46:37 +08:00
优化
This commit is contained in:
@@ -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" />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user