"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { AlertTriangle, CheckCircle2, Clock, ExternalLink, Link2, Loader2, PlayCircle, Plus, Square, Trash2, X } from "lucide-react"; type TaskStatus = "pending" | "running" | "success" | "error"; type Task = { id: string; url: string; status: TaskStatus; startedAt?: number; finishedAt?: number; error?: string; result?: any; }; const extractDouyinLinks = (text: string): string[] => { if (!text) return []; const regex = /https?:\/\/v\.douyin\.com\/\S+/g; // 粗略匹配直到空白 const matches = text.match(regex) ?? []; const trailing = /[)\]】>。,、!!??\s]+$/; // 去掉常见中文/英文结尾符号 const cleaned = matches .map((m) => m.replace(trailing, "")) .map((m) => m.endsWith("/") ? m : m); // 保持原样,通常短链以 / 结尾 // 去重 return Array.from(new Set(cleaned)); }; export default function TasksPage() { const [input, setInput] = useState(""); const [tasks, setTasks] = useState([]); const controllers = useRef>(new Map()); const [openDetails, setOpenDetails] = useState>(new Set()); const [, setTick] = useState(0); // 用于强制更新计时显示 // 设置页面标题 useEffect(() => { document.title = "任务管理 - 抖歪"; return () => { document.title = "抖歪 - 记录当下时代"; }; }, []); const inProgressUrls = useMemo( () => new Set(tasks.filter(t => t.status === "pending" || t.status === "running").map(t => t.url)), [tasks] ); const addTasks = useCallback((urls: string[]) => { if (!urls.length) return; const now = Date.now(); setTasks((prev) => { const existing = new Set(prev.map((t) => t.id)); const notDuplicated = urls.filter(u => !inProgressUrls.has(u)); const newTasks: Task[] = []; for (const url of notDuplicated) { const id = `${now}-${Math.random().toString(36).slice(2, 8)}`; newTasks.push({ id, url, status: "pending" }); } // 新任务添加到最前面 return [...newTasks, ...prev]; }); }, [inProgressUrls]); const handleSubmit = useCallback((e?: React.FormEvent) => { e?.preventDefault(); const urls = extractDouyinLinks(input); if (!urls.length) { alert("未检测到 Douyin 短链,请粘贴包含 https://v.douyin.com/... 的文本"); return; } addTasks(urls); setInput(""); }, [input, addTasks]); const startTask = useCallback(async (task: Task) => { // 若已存在控制器,避免重复启动 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(), error: undefined } : t)); try { const res = await fetch(`/api/fetcher?url=${encodeURIComponent(task.url)}`, { signal: ctrl.signal, method: "GET" }); const data = await res.json().catch(() => null); if (!res.ok) { // 使用后端返回的结构化错误信息 const errorMsg = data?.error || `请求失败: ${res.status}`; const errorCode = data?.code || 'UNKNOWN'; throw new Error(`${errorMsg} (${errorCode})`); } 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)); setTasks(prev => prev.map(t => t.id === task.id ? { ...t, status: "error", finishedAt: Date.now(), error: msg } : t)); } finally { controllers.current.delete(task.id); } }, []); // 自动拉起 pending 任务(使用 effect 防止每次 render 重复触发) useEffect(() => { const pending = tasks.filter(t => t.status === "pending"); 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")); }, []); const toggleOpen = useCallback((id: string) => { setOpenDetails(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }) }, []); const extractedCount = useMemo(() => extractDouyinLinks(input).length, [input]); const formatDuration = (startTime?: number, endTime?: number) => { if (!startTime) return ""; const end = endTime || Date.now(); const secs = Math.max(0, Math.round((end - startTime) / 1000)); if (secs < 60) return `${secs}s`; const m = Math.floor(secs / 60); const s = secs % 60; return `${m}m ${s}s`; }; const StatusBadge = ({ status }: { status: TaskStatus }) => { const base = "inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium"; if (status === 'running') return 进行中; if (status === 'pending') return 待开始; if (status === 'success') return 完成; return 失败; }; return (
{/* 背景渐变(明确置于内容层后面) */}

抖音采集任务

粘贴任意文本,我会自动提取 Douyin 短链并提交到后端处理。

{/* 输入卡片 */}