优化前端,修改返回逻辑,添加视频实时背景,音量倍速持久化
This commit is contained in:
+93
-12
@@ -32,6 +32,7 @@ export default function TasksPage() {
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const controllers = useRef<Map<string, AbortController>>(new Map());
|
||||
const [openDetails, setOpenDetails] = useState<Set<string>>(new Set());
|
||||
const [, setTick] = useState(0); // 用于强制更新计时显示
|
||||
|
||||
const inProgressUrls = useMemo(
|
||||
() => new Set(tasks.filter(t => t.status === "pending" || t.status === "running").map(t => t.url)),
|
||||
@@ -44,12 +45,13 @@ export default function TasksPage() {
|
||||
setTasks((prev) => {
|
||||
const existing = new Set(prev.map((t) => t.id));
|
||||
const notDuplicated = urls.filter(u => !inProgressUrls.has(u));
|
||||
const next: Task[] = [...prev];
|
||||
const newTasks: Task[] = [];
|
||||
for (const url of notDuplicated) {
|
||||
const id = `${now}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
next.push({ id, url, status: "pending" });
|
||||
newTasks.push({ id, url, status: "pending" });
|
||||
}
|
||||
return next;
|
||||
// 新任务添加到最前面
|
||||
return [...newTasks, ...prev];
|
||||
});
|
||||
}, [inProgressUrls]);
|
||||
|
||||
@@ -69,14 +71,18 @@ export default function TasksPage() {
|
||||
if (controllers.current.has(task.id)) return;
|
||||
const ctrl = new AbortController();
|
||||
controllers.current.set(task.id, ctrl);
|
||||
setTasks(prev => prev.map(t => t.id === task.id ? { ...t, status: "running", startedAt: Date.now() } : t));
|
||||
setTasks(prev => prev.map(t => t.id === task.id ? { ...t, status: "running", startedAt: Date.now(), error: undefined } : t));
|
||||
try {
|
||||
const res = await fetch(`/fetcher?url=${encodeURIComponent(task.url)}`, { signal: ctrl.signal, method: "GET" });
|
||||
const data = await res.json().catch(() => null);
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
throw new Error(text || `请求失败: ${res.status}`);
|
||||
// 使用后端返回的结构化错误信息
|
||||
const errorMsg = data?.error || `请求失败: ${res.status}`;
|
||||
const errorCode = data?.code || 'UNKNOWN';
|
||||
throw new Error(`${errorMsg} (${errorCode})`);
|
||||
}
|
||||
const data = await res.json().catch(() => undefined);
|
||||
|
||||
setTasks(prev => prev.map(t => t.id === task.id ? { ...t, status: "success", finishedAt: Date.now(), result: data } : t));
|
||||
} catch (err: any) {
|
||||
const msg = err?.name === 'AbortError' ? '已取消' : (err?.message || String(err));
|
||||
@@ -92,12 +98,33 @@ export default function TasksPage() {
|
||||
pending.forEach((t) => startTask(t));
|
||||
}, [tasks, startTask]);
|
||||
|
||||
// 定时器更新运行中任务的耗时显示
|
||||
useEffect(() => {
|
||||
const hasRunningTasks = tasks.some(t => t.status === "running");
|
||||
if (!hasRunningTasks) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setTick(prev => prev + 1);
|
||||
}, 1000); // 每秒更新一次
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [tasks]);
|
||||
|
||||
const cancelTask = useCallback((id: string) => {
|
||||
const ctrl = controllers.current.get(id);
|
||||
if (ctrl) ctrl.abort();
|
||||
controllers.current.delete(id);
|
||||
}, []);
|
||||
|
||||
const retryTask = useCallback((taskId: string) => {
|
||||
setTasks(prev => prev.map(t => {
|
||||
if (t.id === taskId) {
|
||||
return { ...t, status: "pending" as TaskStatus, error: undefined, result: undefined };
|
||||
}
|
||||
return t;
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const clearFinished = useCallback(() => {
|
||||
setTasks(prev => prev.filter(t => t.status === "pending" || t.status === "running"));
|
||||
}, []);
|
||||
@@ -203,7 +230,25 @@ export default function TasksPage() {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 space-x-2">
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{t.status === 'success' && t.result?.data?.aweme_id && (
|
||||
<a
|
||||
href={`/aweme/${t.result.data.aweme_id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1 rounded-md bg-emerald-600/80 px-2 py-1 text-xs text-white transition-colors hover:bg-emerald-600"
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5"/> 查看作品
|
||||
</a>
|
||||
)}
|
||||
{t.status === 'error' && (
|
||||
<button
|
||||
onClick={() => retryTask(t.id)}
|
||||
className="inline-flex items-center gap-1 rounded-md bg-amber-600/80 px-2 py-1 text-xs text-white transition-colors hover:bg-amber-600"
|
||||
>
|
||||
<PlayCircle className="h-3.5 w-3.5"/> 重试
|
||||
</button>
|
||||
)}
|
||||
{t.status === 'running' && (
|
||||
<button onClick={() => cancelTask(t.id)} className="inline-flex items-center gap-1 rounded-md bg-red-600/80 px-2 py-1 text-xs text-white transition-colors hover:bg-red-600">
|
||||
<Square className="h-3.5 w-3.5"/> 取消
|
||||
@@ -224,13 +269,49 @@ export default function TasksPage() {
|
||||
|
||||
{isOpen && (
|
||||
<div className="mt-3 rounded-md border border-neutral-800/60 bg-neutral-900/60 p-3">
|
||||
{t.status === 'error' && (
|
||||
<div className="mb-2 inline-flex items-center gap-2 rounded border border-red-500/30 bg-red-500/10 px-2 py-1 text-xs text-red-200">
|
||||
<AlertTriangle className="h-4 w-4"/> {t.error}
|
||||
{t.status === 'error' && t.error && (
|
||||
<div className="mb-3 space-y-2">
|
||||
<div className="flex items-start gap-2 rounded-md border border-red-500/30 bg-red-500/10 p-3">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-red-400"/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium text-red-200">爬取失败</div>
|
||||
<div className="mt-1 text-xs text-red-300/90">{t.error}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-neutral-400">
|
||||
<strong>常见原因:</strong>
|
||||
<ul className="ml-4 mt-1 list-disc space-y-0.5">
|
||||
<li>视频已被删除或设为私密</li>
|
||||
<li>网络连接问题或请求超时</li>
|
||||
<li>抖音接口限流或反爬虫</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{typeof t.result !== 'undefined' && (
|
||||
<pre className="max-h-64 overflow-auto rounded bg-neutral-950 p-2 text-xs text-neutral-300">{JSON.stringify(t.result, null, 2)}</pre>
|
||||
<div>
|
||||
{t.result?.data?.aweme && (
|
||||
<div className="mb-3 space-y-2 rounded-md border border-emerald-500/20 bg-emerald-500/5 p-3 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 text-emerald-400"/>
|
||||
<span className="font-medium text-emerald-200">作品信息</span>
|
||||
</div>
|
||||
<div className="space-y-1 text-neutral-300">
|
||||
<div><span className="text-neutral-400">ID:</span> {t.result.data.aweme.aweme_id}</div>
|
||||
<div><span className="text-neutral-400">描述:</span> {t.result.data.aweme.desc || '(无)'}</div>
|
||||
{t.result.data.aweme.author && (
|
||||
<div><span className="text-neutral-400">作者:</span> {t.result.data.aweme.author.nickname}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<details className="group">
|
||||
<summary className="cursor-pointer text-xs text-neutral-400 hover:text-neutral-300">
|
||||
查看完整响应数据 <span className="group-open:hidden">▶</span><span className="hidden group-open:inline">▼</span>
|
||||
</summary>
|
||||
<pre className="mt-2 max-h-64 overflow-auto rounded bg-neutral-950 p-2 text-xs text-neutral-300">{JSON.stringify(t.result, null, 2)}</pre>
|
||||
</details>
|
||||
</div>
|
||||
)}
|
||||
{typeof t.result === 'undefined' && t.status !== 'error' && (
|
||||
<div className="text-xs text-neutral-400">暂无输出</div>
|
||||
|
||||
Reference in New Issue
Block a user