From ce4f6b204fa81e1958dff99e0eafa098b38debd0 Mon Sep 17 00:00:00 2001 From: yibo Date: Fri, 28 Aug 2026 20:12:52 +0800 Subject: [PATCH] h3-console: 任务列表加搜索/筛选/排序,修复只能看到最近50条任务 --- h3-console/server.py | 28 ++++++++++++++++++++++++---- h3-console/static/index.html | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 5 deletions(-) diff --git a/h3-console/server.py b/h3-console/server.py index 1e81c9e..2f170d5 100644 --- a/h3-console/server.py +++ b/h3-console/server.py @@ -465,16 +465,36 @@ async def download_video(jid: str, request: Request): @router.get("/v1/videos") -async def list_videos(limit: int = 50, trash: bool = False): +async def list_videos(limit: int = 50, trash: bool = False, q: str = "", status: str = "", + kind: str = "", sort: str = "desc"): # console 自己的 sqlite 是任务列表的唯一真相来源(本地排队中的任务,gateway 根本不知道它存在, # 没法从 gateway 反查出来)。实时进度由后台 reconciler_loop 持续刷新进 last_known_json, # 这里直接读,不在请求路径里再打一遍 gateway,避免列表接口被拖慢。 # 软删除:deleted_at 非空的任务默认不出现在列表里,?trash=true 反过来只看这些—— # 数据还在同一张表里,删除/恢复就是 UPDATE 一个时间戳,不用挪表、不用真删文件。 - where = "deleted_at IS NOT NULL" if trash else "deleted_at IS NULL" - order = "deleted_at DESC" if trash else "created_at DESC" + where = ["deleted_at IS NOT NULL" if trash else "deleted_at IS NULL"] + params: list = [] + if q: + # prompt 只存在 req_json 这坨 JSON 文本里,没有单独的列——json.dumps 是 + # ensure_ascii=False 存的,中文原样落盘,直接 LIKE 子串匹配就够用,不用上 json_extract。 + where.append("(id LIKE ? OR req_json LIKE ?)") + params += [f"%{q}%", f"%{q}%"] + if status: + where.append("status = ?") + params.append(status) + if kind: + # 前端叫它"类型"(文生/首尾帧/参考图),数据库列叫 partition——是同一个值 + # (见 _partition_of),换个外部参数名单纯是避免跟 /v1/backends 里 GPU 分区的 + # "partition" 混着看。 + where.append("partition = ?") + params.append(kind) + order_col = "deleted_at" if trash else "created_at" + order = f"{order_col} {'ASC' if sort == 'asc' else 'DESC'}" + params.append(limit) with db() as con: - rows = con.execute(f"SELECT * FROM jobs WHERE {where} ORDER BY {order} LIMIT ?", (limit,)).fetchall() + rows = con.execute( + f"SELECT * FROM jobs WHERE {' AND '.join(where)} ORDER BY {order} LIMIT ?", params + ).fetchall() return {"jobs": [_row_view(r) for r in rows]} diff --git a/h3-console/static/index.html b/h3-console/static/index.html index c108766..53b06aa 100644 --- a/h3-console/static/index.html +++ b/h3-console/static/index.html @@ -12,7 +12,15 @@ #sidebar { width: 300px; flex-shrink: 0; border-right: 1px solid #ddd; display: flex; flex-direction: column; height: 100vh; } #sidebar-header { padding: 16px; border-bottom: 1px solid #ddd; } #sidebar-header h1 { font-size: 16px; margin: 0; } + #sidebar-filters { padding: 8px 16px; border-bottom: 1px solid #ddd; display: flex; flex-direction: column; gap: 6px; } + #sidebar-filters input, #sidebar-filters select { font-size: 12px; padding: 4px 6px; width: auto; } + #sidebar-filters .sf-row { display: flex; gap: 6px; } + #sidebar-filters .sf-row select { flex: 1; min-width: 0; } + #job-sort-btn { font-size: 12px; padding: 4px 8px; border: 1px solid #888; background: transparent; + border-radius: 4px; cursor: pointer; white-space: nowrap; } + #job-sort-btn:hover { background: rgba(128,128,128,.15); } #job-list { flex: 1; overflow-y: auto; } + #job-list-footer { padding: 10px 16px; text-align: center; font-size: 11px; opacity: .55; } .job-row { position: relative; padding: 10px 16px; border-bottom: 1px solid #eee; cursor: pointer; } .job-row:hover { background: rgba(128,128,128,.08); } .job-row.selected { background: rgba(74,170,119,.15); border-left: 3px solid #4a7; } @@ -84,6 +92,26 @@ +
` : ''}`; box.appendChild(row); }); + + if (jobsCache.length) { + const footer = document.createElement('div'); + footer.id = 'job-list-footer'; + footer.textContent = noMoreJobs ? '— 没有更多了 —' : '下拉加载更多…'; + box.appendChild(footer); + } else if (filterQ || filterStatus || filterKind) { + const empty = document.createElement('div'); + empty.style.cssText = 'padding:16px;font-size:13px;opacity:.6'; + empty.textContent = '没有匹配的任务'; + box.appendChild(empty); + } } // 任意点击落在某条任务的「⋯」菜单外面,就把菜单收起来——不然点开一个菜单后点别处它会一直挂着 @@ -873,8 +954,13 @@ async function refreshDetail() { async function refreshJobs() { if (viewingTrash) { await loadTrash(); renderSidebar(); return; } try { - const r = await fetch(H3_BASE + '/v1/videos?limit=50'); const d = await r.json(); + const params = new URLSearchParams({ limit: String(loadedLimit), sort: sortOrder }); + if (filterQ) params.set('q', filterQ); + if (filterStatus) params.set('status', filterStatus); + if (filterKind) params.set('kind', filterKind); + const r = await fetch(`${H3_BASE}/v1/videos?${params}`); const d = await r.json(); jobsCache = d.jobs || []; + noMoreJobs = jobsCache.length < loadedLimit; renderSidebar(); // 只有还在变化的任务(排队/运行中)才需要重新拉详情。`refreshDetail()` 是整段 // innerHTML 重建,`done` 之后再重建会把正在播放的