feat(webui): enhanced tasks page with countdown and status dots

This commit is contained in:
xiaoxixi 2026-07-26 22:12:15 +08:00
parent f0ef14099d
commit ee5bc46cc6

View File

@ -10,6 +10,7 @@
let runs = $state({}); let runs = $state({});
let loading = $state(true); let loading = $state(true);
let error = $state(""); let error = $state("");
let tick = $state(0);
async function load() { async function load() {
loading = true; loading = true;
@ -21,7 +22,7 @@
jobs = (await api("/api/jobs")).jobs; jobs = (await api("/api/jobs")).jobs;
runs = Object.fromEntries(await Promise.all(jobs.map(async (job) => [ runs = Object.fromEntries(await Promise.all(jobs.map(async (job) => [
job.id, job.id,
await api(`/api/jobs/${encodeURIComponent(job.id)}/runs?limit=5`).then((value) => value.runs).catch(() => []) await api(`/api/jobs/${encodeURIComponent(job.id)}/runs?limit=10`).then((value) => value.runs).catch(() => [])
]))); ])));
} }
} catch (caught) { } catch (caught) {
@ -36,7 +37,43 @@
load(); load();
} }
onMount(load); function countdown(ts) {
void tick;
if (!ts) return "—";
const ms = ts < 1e12 ? ts * 1000 : ts;
const diff = ms - Date.now();
if (diff <= 0) return "即将运行";
const mins = Math.floor(diff / 60000);
if (mins < 1) return "即将运行";
if (mins < 60) return `${mins} 分钟后`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `${hours} 小时后`;
return `${Math.floor(hours / 24)} 天后`;
}
function elapsed(createdAt) {
void tick;
if (!createdAt) return "";
const ms = createdAt < 1e12 ? createdAt * 1000 : createdAt;
const secs = Math.floor((Date.now() - ms) / 1000);
if (secs < 0) return "";
if (secs < 60) return `${secs}s`;
if (secs < 3600) return `${Math.floor(secs / 60)}m ${secs % 60}s`;
return `${Math.floor(secs / 3600)}h ${Math.floor((secs % 3600) / 60)}m`;
}
function dotColor(status) {
if (status === "completed" || status === "success" || status === "ok") return "var(--signal)";
if (status === "timeout") return "var(--accent)";
if (status === "running") return "var(--info)";
return "var(--danger)";
}
onMount(() => {
load();
const timer = setInterval(() => { tick += 1; }, 30000);
return () => clearInterval(timer);
});
</script> </script>
<section class="page active content-page"> <section class="page active content-page">
@ -54,15 +91,60 @@
{:else if error}<div class="empty-card error-text">{error}</div> {:else if error}<div class="empty-card error-text">{error}</div>
{:else if tab === "background"} {:else if tab === "background"}
{#each tasks as task (task.id)} {#each tasks as task (task.id)}
<article class="card"><div class="card-row"><div><h3>{task.prompt.slice(0, 100)}</h3><div class="meta"><span>{task.session_id}</span><span>{formatTime(task.created_at)}</span><span>{task.tool_calls_count} 次工具调用 · {task.iterations}</span></div></div><StatusBadge status={task.status} /></div>{#if task.result}<div class="details"><p>{task.result}</p></div>{/if}{#if task.error}<p class="error-text">{task.error}</p>{/if}</article> <article class="card">
<div class="card-row">
<div>
<h3>
{#if task.status === "running"}<span class="pulse"></span>{/if}
{task.prompt.slice(0, 100)}
</h3>
<div class="meta">
<span>{task.session_id}</span>
<span>{formatTime(task.created_at)}</span>
{#if task.status === "running"}<span class="elapsed">{elapsed(task.created_at)}</span>{/if}
<span>{task.tool_calls_count} 次工具调用 · {task.iterations}</span>
</div>
</div>
<StatusBadge status={task.status} />
</div>
{#if task.result}<div class="details"><p>{task.result}</p></div>{/if}
{#if task.error}<p class="error-text">{task.error}</p>{/if}
</article>
{:else}<div class="empty-card">暂无后台任务</div>{/each} {:else}<div class="empty-card">暂无后台任务</div>{/each}
{:else} {:else}
{#each jobs as job (job.id)} {#each jobs as job (job.id)}
<article class="card"> <article class="card">
<div class="card-row"><div><h3>{job.name}</h3><p>{job.prompt}</p><div class="meta"><span>{job.job_kind === "monitor" ? "巡检" : "任务"} · {job.delivery_policy}</span><span>{job.channel} · {job.chat_id}</span><span>下次 {formatTime(job.next_run_at)}</span><span>上次 {formatTime(job.last_run_at)}</span></div></div><StatusBadge status={job.enabled ? (job.last_status || "enabled") : "disabled"} /></div> <div class="card-row">
<div>
<h3>{job.name}</h3>
<p>{job.prompt}</p>
<div class="meta">
<span class="mono cron">{job.cron}</span>
<span>{job.job_kind === "monitor" ? "巡检" : "任务"} · {job.delivery_policy}</span>
<span>{job.channel} · {job.chat_id}</span>
<span>下次 {countdown(job.next_run_at)}</span>
<span>上次 {formatTime(job.last_run_at)}</span>
</div>
{#if runs[job.id]?.length}
<div class="status-dots">
{#each runs[job.id] as run}
<span class="dot" style="background:{dotColor(run.status)}" title="{run.status} {formatTime(run.finished_at)}"></span>
{/each}
</div>
{/if}
</div>
<StatusBadge status={job.enabled ? (job.last_status || "enabled") : "disabled"} />
</div>
{#if runs[job.id]?.length}<div class="details">{#each runs[job.id] as run}<div class="card-row run-row"><span class="meta">{formatTime(run.finished_at)} · {run.duration_ms}ms{run.result_kind ? ` · ${run.result_kind}` : ""}{run.delivery_status ? ` · ${run.delivery_status}` : ""}</span><StatusBadge status={run.status} /></div>{/each}</div>{/if} {#if runs[job.id]?.length}<div class="details">{#each runs[job.id] as run}<div class="card-row run-row"><span class="meta">{formatTime(run.finished_at)} · {run.duration_ms}ms{run.result_kind ? ` · ${run.result_kind}` : ""}{run.delivery_status ? ` · ${run.delivery_status}` : ""}</span><StatusBadge status={run.status} /></div>{/each}</div>{/if}
</article> </article>
{:else}<div class="empty-card">暂无定时任务</div>{/each} {:else}<div class="empty-card">暂无定时任务</div>{/each}
{/if} {/if}
</div> </div>
</section> </section>
<style>
.cron { font-size: 11px; color: var(--text-soft); background: var(--code-bg); padding: 1px 5px; border-radius: 4px; border: 1px solid var(--line); }
.status-dots { display: flex; gap: 4px; margin-top: 6px; align-items: center; }
.dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
.elapsed { color: var(--info); font-family: var(--font-mono); font-size: 10px; }
</style>