From 7b4eb5ff6093ba25d11195df819bbd1ab2683b87 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Sat, 24 Oct 2026 22:44:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=89=93=E5=8D=B0=E8=83=BD=E5=8A=9B):=20?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=89=93=E5=8D=B0=E8=BF=9B=E5=BA=A6=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8F=90=E7=A4=BA=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/index.html | 60 +++++++++++++++++----------------- assets/webui/ofdgo.css | 37 +++++++++------------ assets/webui/ofdgo_print.js | 65 +++++++++++++++++++++++-------------- 3 files changed, 88 insertions(+), 74 deletions(-) diff --git a/assets/webui/index.html b/assets/webui/index.html index ea0d8d7..8b250b9 100644 --- a/assets/webui/index.html +++ b/assets/webui/index.html @@ -676,36 +676,38 @@

打印设置

-
diff --git a/assets/webui/ofdgo.css b/assets/webui/ofdgo.css index 5eb1f8b..560d691 100644 --- a/assets/webui/ofdgo.css +++ b/assets/webui/ofdgo.css @@ -39,8 +39,9 @@ } .print-settings { + flex: 1; min-width: 0; - max-height: 100%; + min-height: 0; margin: 0; padding: 0; overflow: auto; @@ -58,11 +59,9 @@ .print-settings [hidden] { display: none !important; } .print-dialog :is(input, select, button):focus-visible { outline-offset: -2px; } .print-field { display: flex; min-width: 0; align-items: center; gap: 6px; } -.print-dialog .print-settings input[type="number"] { min-width: 0; width: 100%; } .print-dialog .print-field > :is(input, select) { flex: 1 1 0; min-width: 0; width: 0; } .print-field > :is(.print-check, button) { flex: 0 0 80px; justify-content: center; } -.print-field:has(#printBackReverse) { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } -.print-field:has(#printBackReverse) .print-check { justify-content: start; } +.print-field:has(#printBackReverse) .print-check:first-child { flex: 1; justify-content: start; } .print-field > span { flex: none; color: var(--text-muted); white-space: nowrap; } .print-dialog .print-settings .print-margins input { padding: 0 3px; text-align: center; } .print-check { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; } @@ -70,38 +69,36 @@ .print-options label { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; } .print-options > label:nth-child(3n) { justify-content: center; } .print-check input, .print-options input { margin: 0; accent-color: var(--accent); } -.print-footer { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 14px; margin-top: 12px; } -.print-footer > .form-status { min-width: 0; margin: 0; overflow-wrap: anywhere; } -.print-dialog .form-actions { grid-template-columns: 80px 80px; margin: 0; } +.print-settings-panel, .print-preview { display: flex; min-width: 0; min-height: 0; flex-direction: column; gap: 12px; } +.print-dialog .form-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.print-preview-stage { position: relative; display: flex; flex: 1; min-height: 0; } +.print-preview-stage .print-progress { position: absolute; bottom: 12px; width: calc(100% - 24px); max-width: 420px; } +.print-progress:has(.progress-label:empty) { display: none; } +.print-progress:has(.progress-spinner[hidden]) { grid-template-columns: minmax(0, 1fr); } +.print-progress .progress-label { white-space: normal; overflow-wrap: anywhere; } .print-preview-page { display: grid; + flex: 1; min-width: 0; - height: 100%; min-height: 0; justify-items: safe center; align-items: safe center; padding: 12px; - overflow: auto; + overflow: hidden; background: #e1e6e9; border: 1px solid var(--line); } .print-preview-page img { display: block; max-width: none; background: white; box-shadow: var(--shadow-soft); } -.print-preview-tools { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 7px; } -.print-dialog .print-preview-tools input { width: 52px; text-align: center; } -.print-dialog .print-preview-tools select { width: 70px; } +.print-preview-tools { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 5px; } #ofdgoPrintDocument { display: none; } @media (max-width: 680px) { .print-content { grid-template-columns: minmax(0, 1fr); height: auto; min-height: 0; } - .print-settings { max-height: 34dvh; } - .print-preview-page { height: 32dvh; min-height: 160px; } - .print-footer { grid-template-columns: minmax(0, 1fr); gap: 8px; } - .print-footer > .form-status:empty { display: none; } - .print-preview-tools { gap: 5px; } - .print-dialog .print-preview-tools input { width: 42px; } - .print-dialog .form-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .print-settings { flex: none; max-height: 34dvh; } + .print-preview-stage { flex: none; height: 32dvh; min-height: 160px; } + .print-preview-tools { justify-content: space-between; } } @media print { @@ -1488,8 +1485,6 @@ button:not(:disabled):hover { .info-custom-row input, .gradient-stop-row input[type="number"], .structure-attribute > input, -.print-preview-tools input, -.print-preview-tools select, .print-options select, .form-row input[type="text"], .form-row input[type="password"], diff --git a/assets/webui/ofdgo_print.js b/assets/webui/ofdgo_print.js index 5905f42..c18c3b7 100644 --- a/assets/webui/ofdgo_print.js +++ b/assets/webui/ofdgo_print.js @@ -1,7 +1,7 @@ export class PrintDialog { constructor(options) { Object.assign(this, options); - this.el = Object.fromEntries(["Panel", "Form", "Settings", "Printer", "Properties", "Copies", "Collation", "Duplex", "BackReverse", "BackRotate", "Contents", "Scope", "Range", "Parity", "Reverse", "Mode", "Paper", "Width", "Height", "Orientation", "Scaling", "Scale", "MarginTop", "MarginRight", "MarginBottom", "MarginLeft", "Grid", "PageOrder", "Gap", "Binding", "BookletSheets", "SheetFrom", "SheetTo", "Overlap", "Sides", "Rotate", "Grayscale", "Borders", "CropMarks", "Raster", "DPI", "TileOnlyLarge", "Labels", "Preview", "Previous", "Next", "PreviewNumber", "PageCount", "Zoom", "Status", "Cancel", "Submit"].map(name => [name, document.querySelector(`#print${name}`)])); + this.el = Object.fromEntries(["Panel", "Form", "Settings", "Printer", "Properties", "Copies", "Collation", "Duplex", "BackReverse", "BackRotate", "Contents", "Scope", "Range", "Parity", "Reverse", "Mode", "Paper", "Width", "Height", "Orientation", "Scaling", "Scale", "MarginTop", "MarginRight", "MarginBottom", "MarginLeft", "Grid", "PageOrder", "Gap", "Binding", "BookletSheets", "SheetFrom", "SheetTo", "Overlap", "Sides", "Rotate", "Grayscale", "Borders", "CropMarks", "Raster", "DPI", "TileOnlyLarge", "Labels", "Preview", "Previous", "Next", "PreviewNumber", "PageControl", "PageCount", "Status", "Spinner", "ProgressTrack", "ProgressBar", "Cancel", "Submit"].map(name => [name, document.querySelector(`#print${name}`)])); this.printerOptions = this.el.Printer.innerHTML; this.paperOptions = this.el.Paper.innerHTML; this.queue = Promise.resolve(); @@ -17,7 +17,11 @@ export class PrintDialog { this.el.Previous.addEventListener("click", () => this.preview(this.side - 1)); this.el.Next.addEventListener("click", () => this.preview(this.side + 1)); this.el.PreviewNumber.addEventListener("change", () => this.preview(Number(this.el.PreviewNumber.value) - 1)); - this.el.Zoom.addEventListener("change", () => this.resize()); + this.el.PreviewNumber.addEventListener("keydown", event => { + if (event.key !== "Enter") return; + event.preventDefault(); + if (!event.repeat) this.el.PreviewNumber.dispatchEvent(new Event("change")); + }); new ResizeObserver(([entry]) => { this.previewSize = entry.contentRect; this.resize(); }).observe(this.el.Preview); window.addEventListener("afterprint", () => { if (this.waiting === "browser") this.finishPrint(); }); window.addEventListener("beforeprint", () => { @@ -53,7 +57,7 @@ export class PrintDialog { this.el.Scope.value = context.selection.length ? "selected" : "all"; this.el.Scope.querySelector('option[value="selected"]').disabled = !context.selection.length; this.el.Contents.value = context.annotations ? "documentAndAnnotations" : "document"; - this.el.Status.textContent = ""; + this.status(); this.el.Preview.replaceChildren(); this.show(this.el.Panel); this.loadDevices(); @@ -72,6 +76,7 @@ export class PrintDialog { if (browser.disabled) this.el.Printer.value = "pdf"; try { if (this.nativeHost) { + this.status("正在读取设备", true); const capabilities = await this.nativeHost.call("print.capabilities", {}); if (!this.current(sequence)) return; this.capabilities = capabilities; @@ -82,7 +87,7 @@ export class PrintDialog { const printer = printers.find(printer => printer.default) || printers[0]; this.el.Printer.value = printer ? `device:${printer.name}` : "pdf"; } - } catch (error) { if (this.current(sequence)) this.el.Status.textContent = error.message; } + } catch (error) { if (this.current(sequence)) this.status(error.message); } finally { if (this.current(sequence)) { this.loading = false; await this.choosePrinter(); } } @@ -93,6 +98,7 @@ export class PrintDialog { const paper = this.el.Paper.value; this.layout = this.job = this.profile = null; this.loading = true; + this.status(); this.controls(); this.el.Paper.innerHTML = this.paperOptions; if (!this.isDevice()) { @@ -109,6 +115,7 @@ export class PrintDialog { this.el.Properties.hidden = true; try { if (this.isDevice()) { + this.status("正在读取纸张", true); const profile = await this.nativeHost.call("print.query", {printer:this.el.Printer.value.slice(7), copies:1, duplex:"simplex"}); if (!this.current(sequence)) return; this.profile = profile; @@ -124,7 +131,7 @@ export class PrintDialog { this.el.Grayscale.disabled = !profile.color; this.el.Properties.hidden = !profile.properties; } else this.el.Grayscale.disabled = false; - } catch (error) { if (this.current(sequence)) { this.el.Status.textContent = error.message; return; } } + } catch (error) { if (this.current(sequence)) { this.status(error.message); return; } } finally { if (this.current(sequence)) { this.loading = false; this.controls(); } } if (this.current(sequence)) this.schedule(); } @@ -133,9 +140,10 @@ export class PrintDialog { if (!this.isDevice() || this.running || this.loading) return; const sequence = this.sequence; this.loading = true; + this.status("正在打开属性", true); this.controls(); try { await this.nativeHost.call("print.properties", {printer:this.el.Printer.value.slice(7)}); } - catch (error) { if (this.current(sequence)) this.el.Status.textContent = error.message; } + catch (error) { if (this.current(sequence)) this.status(error.message); } finally { if (this.current(sequence)) { this.loading = false; await this.choosePrinter(); } } } @@ -215,6 +223,7 @@ export class PrintDialog { const sequence = ++this.sequence; this.layout = null; this.job = null; + this.status(); this.controls(); clearTimeout(this.timer); this.timer = setTimeout(() => { @@ -227,25 +236,26 @@ export class PrintDialog { const context = this.context; try { if (!this.el.Form.reportValidity()) return; + this.status("正在准备预览", true); const deviceOptions = this.isDevice() ? this.deviceOptions() : null; const profile = deviceOptions ? await this.nativeHost.call("print.query", deviceOptions) : null; if (!this.current(sequence)) return; this.profile = profile; const options = this.options(profile); - this.el.Status.textContent = "准备打印…"; const layout = await context.call(this.prepared ? "ofdgoLayoutPrint" : "ofdgoPreparePrint", options); if (!this.current(sequence)) return; this.prepared = true; this.layout = layout; if (this.isDevice()) this.el.Copies.max = Math.min(999, this.profile.maxCopies || 999, layout.copiesLimit > 0 ? layout.copiesLimit : 999); this.job = {deviceOptions, fingerprint:profile?.fingerprint, manual:this.manual(), sides:options.sides, backContent:layout.backContent}; - this.el.Status.textContent = ""; - if (layout.sides.some(side => side.placements.some(placement => placement.clipped))) this.el.Status.textContent = "内容超出可打印区域"; - if (this.manual() && options.sides === "back" && !layout.backContent) this.el.Status.textContent = "背面为空白"; this.remember(); await this.preview(Math.min(this.side, layout.sides.length - 1)); + if (!this.current(sequence) || this.layout !== layout) return; + this.status(); + if (layout.sides.some(side => side.placements.some(placement => placement.clipped))) this.status("内容超出可打印区域"); + if (this.manual() && options.sides === "back" && !layout.backContent) this.status("背面为空白"); } catch (error) { - if (this.current(sequence)) this.el.Status.textContent = error.code === "printDenied" ? "文档禁止打印" : error.message; + if (this.current(sequence)) this.status(error.code === "printDenied" ? "文档禁止打印" : error.message); } finally { if (this.current(sequence)) this.controls(); } @@ -275,19 +285,24 @@ export class PrintDialog { this.previewURL = url; this.el.Preview.replaceChildren(img); this.resize(); - } catch (error) { if (this.current(sequence) && preview === this.previewSequence) { this.el.Status.textContent = error.message; this.layout = null; this.controls(); } } + } catch (error) { if (this.current(sequence) && preview === this.previewSequence) { this.status(error.message); this.layout = null; this.controls(); } } } resize() { - const fit = this.el.Zoom.value === "fit"; - this.el.Preview.style.overflow = fit ? "hidden" : "auto"; const img = this.el.Preview.querySelector("img"), side = this.layout?.sides[this.side]; if (!img || !side || !this.previewSize) return; - const factor = fit ? Math.min(this.previewSize.width / side.width, this.previewSize.height / side.height) : Number(this.el.Zoom.value) * 96 / 25.4; + const factor = Math.min(this.previewSize.width / side.width, this.previewSize.height / side.height); img.style.width = `${Math.max(1, side.width * factor)}px`; img.style.height = `${Math.max(1, side.height * factor)}px`; } + status(text = "", busy = false, completed = 0, total = 0) { + this.el.Status.textContent = total ? `${text} · ${completed} / ${total}` : text; + this.el.Spinner.hidden = !busy; + this.el.ProgressTrack.hidden = !busy || !total; + this.el.ProgressBar.style.width = `${total ? completed / total * 100 : 0}%`; + } + controls() { const count = this.layout?.sides.length || 0, busy = !!this.running || !!this.loading; this.el.Settings.disabled = busy; @@ -298,7 +313,8 @@ export class PrintDialog { this.el.PreviewNumber.disabled = busy || !count; this.el.PreviewNumber.max = count || 1; this.el.PreviewNumber.value = count ? this.side + 1 : 1; - this.el.PageCount.textContent = `/ ${count}`; + this.el.PageCount.textContent = String(count); + this.el.PageControl.style.setProperty("--page-width", `${String(count || 1).length + 0.5}ch`); this.el.Cancel.textContent = busy && !this.waiting ? "取消" : "关闭"; this.el.Cancel.disabled = !!this.waiting; } @@ -328,9 +344,9 @@ export class PrintDialog { this.root.replaceChildren(); const style = document.createElement("style"); this.root.append(style); + this.status("正在准备纸面", true, 0, this.layout.sides.length); for (let i = 0; i < this.layout.sides.length; i++) { if (!this.current(sequence)) throw new DOMException("打印已取消", "AbortError"); - this.el.Status.textContent = `准备纸面 ${i + 1}/${this.layout.sides.length}`; const {svg} = await context.call("ofdgoPrintSide", i); if (!this.current(sequence)) throw new DOMException("打印已取消", "AbortError"); const url = URL.createObjectURL(new Blob([svg], {type:"image/svg+xml"})); @@ -342,6 +358,7 @@ export class PrintDialog { style.textContent += `@page ofdgoPrint${i}{size:${side.width}mm ${side.height}mm;margin:0}`; this.root.append(section); await img.decode(); + if (this.current(sequence)) this.status("正在准备纸面", true, i + 1, this.layout.sides.length); } await document.fonts.ready; if (!this.current(sequence)) throw new DOMException("打印已取消", "AbortError"); @@ -349,7 +366,7 @@ export class PrintDialog { document.title = context.name; this.waiting = this.nativeHost ? "native" : "browser"; this.controls(); - this.el.Status.textContent = job.deviceOptions ? "提交打印…" : "打开打印…"; + this.status(job.deviceOptions ? "正在提交打印" : "正在打开打印", true); if (job.deviceOptions) { const result = await this.nativeHost.call("print", {...job.deviceOptions, fingerprint:job.fingerprint}); if (this.current(sequence)) { @@ -357,7 +374,7 @@ export class PrintDialog { if (!result?.submitted) throw new Error("打印作业未被接受"); if (job.manual && job.sides === "front") { if (job.backContent) { this.el.Sides.value = "back"; this.schedule(); } - else this.el.Status.textContent = "背面为空白"; + else this.status("背面为空白"); } } } else if (this.nativeHost) { @@ -372,7 +389,7 @@ export class PrintDialog { } catch (error) { if (this.current(sequence)) { this.finishPrint(); - this.el.Status.textContent = error.name === "AbortError" ? "打印已取消" : error.message; + this.status(error.name === "AbortError" ? "打印已取消" : error.message); } } finally { if (this.current(sequence)) { @@ -387,7 +404,7 @@ export class PrintDialog { this.root?.replaceChildren(); for (const url of this.urls) URL.revokeObjectURL(url); this.urls = []; - this.el.Status.textContent = ""; + this.status(); this.waiting = this.running = false; this.controls(); } @@ -397,14 +414,14 @@ export class PrintDialog { const sequence = this.sequence, context = this.context; this.running = true; this.controls(); - this.el.Status.textContent = "生成文件…"; + this.status("正在生成文件", true); try { const result = await context.call("ofdgoPrintPDF"); if (this.current(sequence)) { await this.download(result.bytes, "application/pdf", `${context.name.replace(/\.[^.]+$/, "")}-打印.pdf`); - if (this.current(sequence)) this.el.Status.textContent = ""; + if (this.current(sequence)) this.status(); } - } catch (error) { if (this.current(sequence)) this.el.Status.textContent = error.message; } + } catch (error) { if (this.current(sequence)) this.status(error.message); } finally { if (this.current(sequence)) { this.running = false; this.controls(); } } }