- 找到 {results.length} 个结果
+ 找到{" "}
+
+ {results.length}
+ {" "}
+ 个结果
{/* 单列列表布局 */}
{results.map((item) => {
- const content = item.type === 'video' ? item.video : item.imagePost;
+ const content =
+ item.type === "video" ? item.video : item.imagePost;
if (!content) return null;
return (
@@ -202,12 +218,14 @@ export default function SearchClient({ initialQuery }: { initialQuery: string })
{/* 类型标签 */}
-
- {item.type === 'video' ? '视频' : '图文'}
+
+ {item.type === "video" ? "视频" : "图文"}
@@ -216,7 +234,10 @@ export default function SearchClient({ initialQuery }: { initialQuery: string })
{/* 描述 */}
-
+
{content.desc || "无描述"}
@@ -245,7 +266,7 @@ export default function SearchClient({ initialQuery }: { initialQuery: string })
className="text-xs text-white/70 bg-white/5 rounded-lg p-2 leading-relaxed"
dangerouslySetInnerHTML={{ __html: item.snippet }}
style={{
- wordBreak: 'break-word',
+ wordBreak: "break-word",
}}
/>
diff --git a/app/search/page.tsx b/app/search/page.tsx
index 184116c..f4286a2 100644
--- a/app/search/page.tsx
+++ b/app/search/page.tsx
@@ -2,7 +2,11 @@
import { Suspense } from "react";
import SearchClient from "./SearchClient";
-export default function Page({ searchParams }: { searchParams: { q?: string } }) {
+export default function Page({
+ searchParams,
+}: {
+ searchParams: { q?: string };
+}) {
const q = typeof searchParams.q === "string" ? searchParams.q : "";
return (
Loading…}>
diff --git a/app/tasks/page.tsx b/app/tasks/page.tsx
index 19005d3..18d4119 100644
--- a/app/tasks/page.tsx
+++ b/app/tasks/page.tsx
@@ -1,7 +1,20 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { AlertTriangle, CheckCircle2, Clipboard, Clock, ExternalLink, Link2, Loader2, PlayCircle, Plus, Square, Trash2, X } from "lucide-react";
+import {
+ AlertTriangle,
+ CheckCircle2,
+ Clipboard,
+ Clock,
+ ExternalLink,
+ Link2,
+ Loader2,
+ PlayCircle,
+ Plus,
+ Square,
+ Trash2,
+ X,
+} from "lucide-react";
type TaskStatus = "pending" | "running" | "success" | "error";
@@ -22,7 +35,7 @@ const extractDouyinLinks = (text: string): string[] => {
const trailing = /[)\]】>。,、!!??\s]+$/; // 去掉常见中文/英文结尾符号
const cleaned = matches
.map((m) => m.replace(trailing, ""))
- .map((m) => m.endsWith("/") ? m : m); // 保持原样,通常短链以 / 结尾
+ .map((m) => (m.endsWith("/") ? m : m)); // 保持原样,通常短链以 / 结尾
// 去重
return Array.from(new Set(cleaned));
};
@@ -43,43 +56,58 @@ export default function TasksPage() {
}, []);
const inProgressUrls = useMemo(
- () => new Set(tasks.filter(t => t.status === "pending" || t.status === "running").map(t => t.url)),
- [tasks]
+ () =>
+ 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 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 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 handlePasteAndAdd = useCallback(async () => {
try {
const text = await navigator.clipboard.readText();
const urls = extractDouyinLinks(text);
if (!urls.length) {
- alert("剪贴板中未检测到 Douyin 短链,请复制包含 https://v.douyin.com/... 的文本");
+ alert(
+ "剪贴板中未检测到 Douyin 短链,请复制包含 https://v.douyin.com/... 的文本",
+ );
return;
}
addTasks(urls);
@@ -94,22 +122,44 @@ 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(), error: undefined } : t));
+ 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 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';
+ 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));
+
+ 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));
+ 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);
}
@@ -117,17 +167,17 @@ export default function TasksPage() {
// 自动拉起 pending 任务(使用 effect 防止每次 render 重复触发)
useEffect(() => {
- const pending = tasks.filter(t => t.status === "pending");
+ const pending = tasks.filter((t) => t.status === "pending");
pending.forEach((t) => startTask(t));
}, [tasks, startTask]);
// 定时器更新运行中任务的耗时显示
useEffect(() => {
- const hasRunningTasks = tasks.some(t => t.status === "running");
+ const hasRunningTasks = tasks.some((t) => t.status === "running");
if (!hasRunningTasks) return;
const timer = setInterval(() => {
- setTick(prev => prev + 1);
+ setTick((prev) => prev + 1);
}, 1000); // 每秒更新一次
return () => clearInterval(timer);
@@ -140,27 +190,40 @@ export default function TasksPage() {
}, []);
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;
- }));
+ 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"));
+ setTasks((prev) =>
+ prev.filter((t) => t.status === "pending" || t.status === "running"),
+ );
}, []);
const toggleOpen = useCallback((id: string) => {
- setOpenDetails(prev => {
+ setOpenDetails((prev) => {
const next = new Set(prev);
- if (next.has(id)) next.delete(id); else next.add(id);
+ if (next.has(id)) next.delete(id);
+ else next.add(id);
return next;
- })
+ });
}, []);
- const extractedCount = useMemo(() => extractDouyinLinks(input).length, [input]);
+ const extractedCount = useMemo(
+ () => extractDouyinLinks(input).length,
+ [input],
+ );
const formatDuration = (startTime?: number, endTime?: number) => {
if (!startTime) return "";
@@ -173,11 +236,31 @@ export default function TasksPage() {
};
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
失败;
+ 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 (
@@ -192,11 +275,16 @@ export default function TasksPage() {
抖音采集任务
-
粘贴任意文本,我会自动提取 Douyin 短链并提交到后端处理。
+
+ 粘贴任意文本,我会自动提取 Douyin 短链并提交到后端处理。
+
{/* 输入卡片 */}
-