优化前端,修改返回逻辑,添加视频实时背景,音量倍速持久化

This commit is contained in:
2025-10-21 16:33:16 +08:00
parent 9d1598a5ab
commit 200283b21b
231 changed files with 744 additions and 62 deletions
+93 -12
View File
@@ -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>