From 8a06236dab2ab40909589612a9ad6a09214694ad Mon Sep 17 00:00:00 2001 From: wuxu Date: Sat, 20 Jun 2026 13:54:22 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- webgui/web/assets/js/app.js | 12 +- webgui/web/assets/js/views/browser.js | 6 +- webgui/web/assets/js/views/configure.js | 6 +- webgui/web/assets/js/views/jobs.js | 165 +++++++++++------------- webgui/web/assets/js/views/remotes.js | 6 +- webgui/web/assets/styles/components.css | 163 ++++++++++++++++++++--- webgui/web/index.html | 4 +- 7 files changed, 237 insertions(+), 125 deletions(-) diff --git a/webgui/web/assets/js/app.js b/webgui/web/assets/js/app.js index 77f86e8..c11f359 100644 --- a/webgui/web/assets/js/app.js +++ b/webgui/web/assets/js/app.js @@ -2,15 +2,15 @@ // the top-nav active state in sync, and re-renders chrome strings when // the locale changes. -import { escapeHtml, onRoute } from "./state.js?v=security-fixes-1"; -import { t, currentLocale, setLocale, onLocale } from "./i18n.js?v=security-fixes-1"; -import { renderRemotes } from "./views/remotes.js?v=security-fixes-1"; -import { renderBrowse } from "./views/browser.js?v=security-fixes-1"; -import { renderJobs, renderNewJob, stopJobPolling } from "./views/jobs.js?v=security-fixes-1"; +import { escapeHtml, onRoute } from "./state.js?v=job-layout-1"; +import { t, currentLocale, setLocale, onLocale } from "./i18n.js?v=job-layout-1"; +import { renderRemotes } from "./views/remotes.js?v=job-layout-1"; +import { renderBrowse } from "./views/browser.js?v=job-layout-1"; +import { renderJobs, renderNewJob, stopJobPolling } from "./views/jobs.js?v=job-layout-1"; import { renderConfigureNew, renderConfigureEdit, -} from "./views/configure.js?v=security-fixes-1"; +} from "./views/configure.js?v=job-layout-1"; const views = { remotes: renderRemotes, diff --git a/webgui/web/assets/js/views/browser.js b/webgui/web/assets/js/views/browser.js index 1e14d3c..73bb245 100644 --- a/webgui/web/assets/js/views/browser.js +++ b/webgui/web/assets/js/views/browser.js @@ -1,8 +1,8 @@ // views/browser.js — file/folder listing with breadcrumbs, mkdir, upload, delete, rename. -import { post, uploadFile, downloadURL } from "../rc.js?v=security-fixes-1"; -import { escapeHtml, toast, formatBytes, formatTime } from "../state.js?v=security-fixes-1"; -import { t } from "../i18n.js?v=security-fixes-1"; +import { post, uploadFile, downloadURL } from "../rc.js?v=job-layout-1"; +import { escapeHtml, toast, formatBytes, formatTime } from "../state.js?v=job-layout-1"; +import { t } from "../i18n.js?v=job-layout-1"; export async function renderBrowse({ remote, path }) { const app = document.getElementById("app"); diff --git a/webgui/web/assets/js/views/configure.js b/webgui/web/assets/js/views/configure.js index 5c82d27..d892a0c 100644 --- a/webgui/web/assets/js/views/configure.js +++ b/webgui/web/assets/js/views/configure.js @@ -9,9 +9,9 @@ // OAuth backends (option named "token" with IsPassword) get a banner // and disabled submit — user must run `rclone config` in a terminal. -import { post } from "../rc.js?v=security-fixes-1"; -import { escapeHtml, getState, setState, toast } from "../state.js?v=security-fixes-1"; -import { t } from "../i18n.js?v=security-fixes-1"; +import { post } from "../rc.js?v=job-layout-1"; +import { escapeHtml, getState, setState, toast } from "../state.js?v=job-layout-1"; +import { t } from "../i18n.js?v=job-layout-1"; // --- Route entrypoints --- diff --git a/webgui/web/assets/js/views/jobs.js b/webgui/web/assets/js/views/jobs.js index 69698dc..a9e8d3a 100644 --- a/webgui/web/assets/js/views/jobs.js +++ b/webgui/web/assets/js/views/jobs.js @@ -1,14 +1,14 @@ // views/jobs.js — submit sync/copy/move jobs and manage recurring transfers. -import { post } from "../rc.js?v=security-fixes-1"; +import { post } from "../rc.js?v=job-layout-1"; import { toast, formatBytes, formatSpeed, formatDuration, escapeHtml, -} from "../state.js?v=security-fixes-1"; -import { t } from "../i18n.js?v=security-fixes-1"; +} from "../state.js?v=job-layout-1"; +import { t } from "../i18n.js?v=job-layout-1"; import { createOneTimeJob, createRecurringJob, @@ -21,7 +21,7 @@ import { runRecurringJobNow, stopOneTimeJob, stopRecurringJob, -} from "../jobs_api.js?v=security-fixes-1"; +} from "../jobs_api.js?v=job-layout-1"; let pollTimer = null; const LOCAL_FS_VALUE = "__local__"; @@ -537,78 +537,37 @@ function renderJobTable(statuses) { return bTime - aTime; }); - const rows = statuses.map(renderJobRows).join(""); + const rows = statuses.map(renderJobRow).join(""); return ` - - - - - - - - - - - - - - - - - ${rows} - -
${t("jobs.col.id")}${t("jobs.col.job")}${t("jobs.col.status")}${t("jobs.col.next_run")}${t("jobs.col.progress")}${t("jobs.col.speed")}${t("jobs.col.eta")}${t("jobs.col.files")}${t("jobs.col.errors")}${t("jobs.col.action")}
+
+ ${rows} +
`; } -function renderJobRows(s) { - const row = renderJobRow(s); - if (!expandedJobs.has(jobKey(s))) { - return row; - } - return row + renderJobDetailsRow(s); -} - function renderJobRow(job) { const snapshot = job.statusSnapshot || {}; const stats = transferStats(snapshot); - const id = job.id; const key = jobKey(job); const running = !!job.running; const jobid = job.currentJobid || job.lastJobid; const displayedSpeed = stats.speed || averageSpeed(stats); const displayedEta = running ? stats.eta : 0; + const expanded = expandedJobs.has(key); const badge = renderStatusBadge(job.status); - const jobCell = ` -
- ${escapeHtml(job.action)} - - ${escapeHtml(job.src)} - - ${escapeHtml(job.dst)} - -
`; - - const scheduleCell = ` -
- ${escapeHtml(job.kind === "once" ? t("jobs.schedule_once") : formatDateTime(job.nextRunAt))} - ${escapeHtml(formatSchedule(job))} -
- `; - const pct = stats.totalBytes > 0 ? Math.min(100, (stats.bytes / stats.totalBytes) * 100) : 0; const progress = ` -
+
- + ${formatBytes(stats.bytes)} / ${formatBytes(stats.totalBytes)}
`; - const detailBtn = ``; + const detailBtn = ``; const startBtn = running ? "" : ``; @@ -618,28 +577,50 @@ function renderJobRow(job) { const deleteBtn = ``; return ` - - - ${escapeHtml(displayJobId(job))} - ${jobid ? `
job ${escapeHtml(jobid)}` : ""} - - ${jobCell} - ${badge} - ${scheduleCell} - ${progress} - ${escapeHtml(formatSpeed(displayedSpeed))} - ${escapeHtml(formatDuration(displayedEta))} - ${escapeHtml(formatTransferCount(stats))} - ${escapeHtml(String(stats.errors || 0))} - +
+
+
+
+ ${escapeHtml(displayJobId(job))} + ${jobid ? `job ${escapeHtml(jobid)}` : ""} +
+ ${badge} +
+
+
+ ${escapeHtml(job.action)} + ${escapeHtml(job.src)} +
+
+ + ${escapeHtml(job.dst)} +
+
+
+ ${escapeHtml(job.kind === "once" ? t("jobs.schedule_once") : formatDateTime(job.nextRunAt))} + ${escapeHtml(formatSchedule(job))} +
+
+
+
+ ${t("jobs.col.progress")} + ${progress} +
+
+ ${metricHtml(t("jobs.col.speed"), formatSpeed(displayedSpeed))} + ${metricHtml(t("jobs.col.eta"), formatDuration(displayedEta))} + ${metricHtml(t("jobs.col.files"), formatTransferCount(stats))} + ${metricHtml(t("jobs.col.errors"), String(stats.errors || 0))} +
${detailBtn} ${startBtn} ${stopBtn} ${deleteBtn}
- - +
+ ${expanded ? renderJobDetails(job) : ""} +
`; } @@ -661,7 +642,16 @@ function renderStatusBadge(status) { } } -function renderJobDetailsRow(job) { +function metricHtml(label, value) { + return ` +
+ ${escapeHtml(label)} + ${escapeHtml(value)} +
+ `; +} + +function renderJobDetails(job) { const snapshot = job.statusSnapshot || {}; const stats = transferStats(snapshot); const jobid = job.currentJobid || job.lastJobid || ""; @@ -688,27 +678,22 @@ function renderJobDetailsRow(job) { : ""; return ` - - - -
-
- ${details.map(([label, value]) => ` -
-
${escapeHtml(label)}
-
${escapeHtml(value)}
-
- `).join("")} -
- ${output ? ` -
- ${t("jobs.detail_output")} -
${escapeHtml(output)}
-
- ` : ""} +
+
+ ${details.map(([label, value]) => ` +
+
${escapeHtml(label)}
+
${escapeHtml(value)}
+
+ `).join("")} +
+ ${output ? ` +
+ ${t("jobs.detail_output")} +
${escapeHtml(output)}
- - + ` : ""} +
`; } diff --git a/webgui/web/assets/js/views/remotes.js b/webgui/web/assets/js/views/remotes.js index 226b495..204a5e0 100644 --- a/webgui/web/assets/js/views/remotes.js +++ b/webgui/web/assets/js/views/remotes.js @@ -1,8 +1,8 @@ // views/remotes.js — connector-tile grid of configured remotes with CRUD. -import { post } from "../rc.js?v=security-fixes-1"; -import { escapeHtml, getState, setState, toast } from "../state.js?v=security-fixes-1"; -import { t } from "../i18n.js?v=security-fixes-1"; +import { post } from "../rc.js?v=job-layout-1"; +import { escapeHtml, getState, setState, toast } from "../state.js?v=job-layout-1"; +import { t } from "../i18n.js?v=job-layout-1"; export async function renderRemotes() { const app = document.getElementById("app"); diff --git a/webgui/web/assets/styles/components.css b/webgui/web/assets/styles/components.css index 8ab07c4..7342369 100644 --- a/webgui/web/assets/styles/components.css +++ b/webgui/web/assets/styles/components.css @@ -865,46 +865,148 @@ margin-bottom: var(--space-md); } -/* ---------- Job cell with action + src→dst ---------- */ -.job-cell { +/* ---------- Jobs ---------- */ +.job-list { + display: grid; + gap: var(--space-sm); +} + +.job-item { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(320px, 380px); + gap: var(--space-lg); + padding: var(--space-lg); + border-bottom: 1px solid var(--color-hairline-soft); +} + +.job-item:last-child { + border-bottom: none; +} + +.job-main { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: var(--space-md); + min-width: 0; +} + +.job-head { display: flex; flex-direction: column; - gap: 2px; - min-width: 200px; + align-items: flex-start; + gap: var(--space-xs); } -.job-cell .job-action { +.job-id { + display: grid; + gap: 2px; + min-width: 58px; + color: var(--color-muted); + font: var(--typo-code); +} + +.job-id small { + color: var(--color-muted-soft); + font: var(--typo-caption); +} + +.job-route { + display: grid; + gap: var(--space-xs); + min-width: 0; +} + +.job-route-line { + display: grid; + grid-template-columns: 44px minmax(0, 1fr); + align-items: start; + gap: var(--space-xs); +} + +.job-route-line span { + color: var(--color-muted); font: var(--typo-caption-uppercase); letter-spacing: var(--typo-caption-uppercase-tracking); - color: var(--color-muted); + text-transform: uppercase; } -.job-cell .job-paths { - font: var(--typo-body-sm); +.job-route-line code { color: var(--color-body); - word-break: break-all; + font: var(--typo-code); + white-space: normal; + overflow-wrap: anywhere; } -.job-cell .job-paths .arrow { - color: var(--color-muted-soft); - margin: 0 var(--space-xxs); +.job-schedule { + grid-column: 2; + display: flex; + gap: var(--space-sm); + flex-wrap: wrap; + color: var(--color-muted); + font: var(--typo-caption); +} + +.job-side { + display: grid; + align-content: start; + gap: var(--space-sm); + min-width: 0; +} + +.job-progress-wrap { + display: grid; + gap: var(--space-xs); +} + +.job-metric-label, +.job-metric span { + color: var(--color-muted); + font: var(--typo-caption); +} + +.job-progress { + display: grid; + gap: 4px; +} + +.job-progress-text { + color: var(--color-muted); + font: var(--typo-code); +} + +.job-metrics { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: var(--space-xs); +} + +.job-metric { + display: grid; + gap: 2px; + min-width: 0; +} + +.job-metric strong { + color: var(--color-body); + font: var(--typo-code); + font-weight: 400; + overflow-wrap: anywhere; } .job-actions { - display: inline-flex; + display: flex; justify-content: flex-end; gap: var(--space-xs); flex-wrap: wrap; } -.job-detail-row { - background-color: var(--color-surface-soft); -} - .job-detail { + grid-column: 1 / -1; display: grid; gap: var(--space-sm); - padding: var(--space-sm) 0; + padding: var(--space-md); + background-color: var(--color-surface-soft); + border-radius: var(--radius-sm); } .job-detail dl { @@ -964,6 +1066,15 @@ .form-grid { grid-template-columns: 1fr; } + .job-item { + grid-template-columns: 1fr; + } + .job-side { + grid-template-columns: 1fr; + } + .job-actions { + justify-content: flex-start; + } } @media (max-width: 768px) { @@ -982,4 +1093,20 @@ .top-nav nav { display: none; } + .job-item { + padding: var(--space-md); + } + .job-main { + grid-template-columns: 1fr; + } + .job-schedule { + grid-column: 1; + } + .job-metrics { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .job-detail dl, + .job-detail dl div { + grid-template-columns: 1fr; + } } diff --git a/webgui/web/index.html b/webgui/web/index.html index 3b62f3e..5ea0c5e 100644 --- a/webgui/web/index.html +++ b/webgui/web/index.html @@ -7,7 +7,7 @@ - +
@@ -80,6 +80,6 @@
- +