-
搜索
-
) => setQ(e.target.value)}
- />
+
+ 搜索
+
+
+
+ {aiOn ? (
+
+ ) : (
+
) => setQ(e.target.value)}
+ />
+ )}
+
+ {intent && (
+
+ AI 解析:
+ {JSON.stringify(intent)}
+
+ )}
+
{loading &&
搜索中…
}
{items.map((m) => (
diff --git a/web/src/pages/TasksPage.tsx b/web/src/pages/TasksPage.tsx
new file mode 100644
index 0000000..e9c1624
--- /dev/null
+++ b/web/src/pages/TasksPage.tsx
@@ -0,0 +1,125 @@
+import { useEffect, useState } from 'react'
+import { Activity } from 'lucide-react'
+
+import { tasksAPI, type TasksSnapshot } from '../api/tasks'
+
+function fmtBytes(n: number): string {
+ if (!n || n <= 0) return '0 B'
+ const u = ['B', 'KB', 'MB', 'GB', 'TB']
+ let v = n
+ let i = 0
+ while (v >= 1024 && i < u.length - 1) {
+ v /= 1024
+ i++
+ }
+ return `${v.toFixed(2)} ${u[i]}`
+}
+
+// TasksPage shows everything the backend is doing right now: ffmpeg
+// transcodes + qBittorrent downloads. Refreshes every 3 s.
+export function TasksPage() {
+ const [snap, setSnap] = useState
(null)
+
+ useEffect(() => {
+ let cancelled = false
+ const tick = () =>
+ tasksAPI.snapshot().then((s) => {
+ if (!cancelled) setSnap(s)
+ })
+ void tick()
+ const id = window.setInterval(tick, 3_000)
+ return () => {
+ cancelled = true
+ window.clearInterval(id)
+ }
+ }, [])
+
+ if (!snap) return 加载中…
+
+ const torrents = snap.torrents ?? []
+
+ return (
+
+
+
+
+ 转码任务
+ {snap.transcodes.length === 0 && 暂无运行中转码。
}
+ {snap.transcodes.length > 0 && (
+
+
+
+ | 媒体 ID |
+ 编码器 |
+ 开始时间 |
+ 就绪 |
+
+
+
+ {snap.transcodes.map((t) => (
+
+ | {t.media_id} |
+ {t.encoder || 'libx264'} |
+ {new Date(t.started_at).toLocaleTimeString()} |
+
+ {t.playlist_ok ? (
+
+ ready
+
+ ) : (
+
+ starting
+
+ )}
+ |
+
+ ))}
+
+
+ )}
+
+
+
+ 下载任务
+ {torrents.length === 0 && 暂无运行中下载。
}
+ {torrents.length > 0 && (
+
+
+
+ | 名称 |
+ 状态 |
+ 进度 |
+ 体积 |
+
+
+
+ {torrents.map((t) => (
+
+ |
+ {t.name}
+ |
+ {t.state} |
+
+
+
+ {(t.progress * 100).toFixed(1)}%
+
+ |
+ {fmtBytes(t.size)} |
+
+ ))}
+
+
+ )}
+
+
+ )
+}