feat(前端界面): 新增打印按钮
build.yaml / build (push) Successful in 47s

This commit is contained in:
xiaoqidun committed 2026-09-10 14:10:07 +08:00
1 parent c3d8c0e8a0
commit 52c02d55ef
3 files changed
+109 -35

No files matched your search

+98 -34
View File
@@ -10,7 +10,8 @@ const STATUS = {
engine: "正在准备引擎",
recovering: "正在恢复引擎",
fonts: "正在匹配字体",
exporting: "正在导出文档 PDF",
exporting: "正在导出文档",
printing: "正在准备打印",
pageExporting: "正在导出单页",
};
const WASM_CALLBACKS = [
@@ -28,6 +29,7 @@ let wasmPromise = null;
let wasmModule = null;
let wasmRecoveryTimer = 0;
let fontDatabase = null;
let printFrame = null;
const state = {
ready: false,
@@ -64,6 +66,7 @@ const state = {
thumbnailObserver: null,
visibleThumbnails: new Set(),
exportFormats: [],
outputting: false,
showPages: !COMPACT_LAYOUT.matches,
showMeta: !COMPACT_LAYOUT.matches,
};
@@ -93,6 +96,7 @@ const el = {
pageExportFormat: document.querySelector("#pageExportFormat"),
exportPageButton: document.querySelector("#exportPageButton"),
exportButton: document.querySelector("#exportButton"),
printButton: document.querySelector("#printButton"),
emptyState: document.querySelector("#emptyState"),
progressPanel: document.querySelector("#progressPanel"),
progressLabel: document.querySelector("#progressLabel"),
@@ -143,7 +147,8 @@ el.continuousButton.addEventListener("click", toggleContinuous);
el.annotationButton.addEventListener("click", toggleAnnotations);
el.pageExportFormat.addEventListener("change", () => updateDPIControl());
el.exportPageButton.addEventListener("click", exportCurrentPage);
el.exportButton.addEventListener("click", exportPDF);
el.exportButton.addEventListener("click", () => outputPDF(false));
el.printButton.addEventListener("click", () => outputPDF(true));
el.refreshAppButton.addEventListener("click", refreshApplication);
el.pageInput.addEventListener("change", () => {
const page = Number.parseInt(el.pageInput.value, 10);
@@ -692,7 +697,7 @@ async function loadLocalFonts() {
return;
}
if (!canReadLocalFonts()) {
setStatus("当前浏览器不支持读取系统字体");
setStatus("不支持读取系统字体");
return;
}
setBusy(true, state.doc ? "正在匹配字体" : "正在请求授权", 12, state.doc ? STATUS.fonts : "正在请求授权");
@@ -708,7 +713,7 @@ async function loadLocalFonts() {
}
} catch (err) {
if (err && err.name === "NotAllowedError") {
setStatus("未授权读取系统字体");
setStatus("系统字体未授权");
} else {
setStatus(String(err.message || err));
}
@@ -727,7 +732,7 @@ async function requestLocalFontsBeforeOpen() {
setStatus(available.length ? `已授权 ${available.length} 个系统字体` : "未读取到系统字体");
} catch (err) {
if (err && err.name === "NotAllowedError") {
setStatus("未授权读取系统字体");
setStatus("系统字体未授权");
return;
}
setStatus(String(err.message || err));
@@ -766,7 +771,7 @@ async function autoLoadDocumentLocalFonts(openSeq) {
return await loadDocumentLocalFonts(available, openSeq);
} catch (err) {
if (err && err.name === "NotAllowedError") {
setStatus("未授权读取系统字体");
setStatus("系统字体未授权");
return false;
}
setStatus(String(err.message || err));
@@ -786,7 +791,7 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) {
}
if (!docNames.length) {
state.localFonts = [];
setStatus("OFD 字体均为内嵌");
setStatus("文档字体均为内嵌");
updateFontSummary();
renderFontList();
return false;
@@ -796,7 +801,7 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) {
if (!selected.length) {
selected = selectLocalFonts(available, [], 6);
}
const emptyStatus = available.length === 0 ? "未读取到系统字体" : "未匹配到文档所需字体";
const emptyStatus = available.length === 0 ? "未读取到系统字体" : "未匹配到所需字体";
const fonts = [];
for (let i = 0; i < selected.length; i += 1) {
setProgress(`正在读取字体 ${i + 1}/${selected.length}`, 20 + Math.round(i / selected.length * 60));
@@ -1093,7 +1098,7 @@ function fontSourceText(source) {
function updateLocalFontButton() {
const supported = canReadLocalFonts();
el.localFontButton.disabled = !supported;
el.localFontButton.title = supported ? "授权读取浏览器可访问的系统字体" : "当前浏览器不支持读取系统字体";
el.localFontButton.title = supported ? "读取系统字体" : "不支持读取系统字体";
updateFontPermissionHint();
}
@@ -1273,19 +1278,23 @@ async function renderPage(index, options = {}) {
}
}
async function exportPDF() {
if (!state.doc) {
async function outputPDF(print) {
if (!state.doc || state.outputting) {
return;
}
if (print && !navigator.pdfViewerEnabled) {
setStatus("请导出 PDF 后打印");
return;
}
const openSeq = state.openSeq;
setExportControlsDisabled(true);
setBusy(true, "正在准备文档 PDF", 18, STATUS.exporting);
setOutputting(true);
setBusy(true, "正在准备 PDF", 18, print ? STATUS.printing : STATUS.exporting);
try {
await waitForPaint();
if (openSeq !== state.openSeq) {
return;
}
setProgress("正在生成文档 PDF", 45);
setProgress("正在生成 PDF", 45);
await waitForPaint();
if (openSeq !== state.openSeq) {
return;
@@ -1294,10 +1303,18 @@ async function exportPDF() {
if (openSeq !== state.openSeq) {
return;
}
setProgress("正在保存文档 PDF", 86);
const bytes = result.bytes;
downloadBytes(bytes, "application/pdf", pdfFileName());
setStatus(`文档 PDF 已导出 ${formatBytes(result.size || bytes.length)}`);
if (print) {
setProgress("正在加载 PDF", 86);
await printPDF(bytes, openSeq);
if (openSeq === state.openSeq) {
setStatus("已请求打印");
}
} else {
setProgress("正在保存 PDF", 86);
downloadBytes(bytes, "application/pdf", pdfFileName());
setStatus(`PDF 已导出 ${formatBytes(result.size || bytes.length)}`);
}
} catch (err) {
if (openSeq === state.openSeq) {
showError(err, false);
@@ -1305,13 +1322,60 @@ async function exportPDF() {
} finally {
if (openSeq === state.openSeq) {
setBusy(false);
updateControls();
}
setOutputting(false);
}
}
function clearPrintFrame() {
if (!printFrame) {
return;
}
URL.revokeObjectURL(printFrame.src);
printFrame.remove();
printFrame = null;
}
function printPDF(bytes, openSeq) {
clearPrintFrame();
const frame = document.createElement("iframe");
frame.className = "print-frame";
frame.title = "文档打印";
frame.tabIndex = -1;
frame.setAttribute("aria-hidden", "true");
frame.src = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
printFrame = frame;
return new Promise((resolve, reject) => {
const clear = () => {
if (printFrame === frame) {
clearPrintFrame();
}
};
const fail = () => {
clear();
reject(new Error("请导出 PDF 后打印"));
};
frame.onload = () => {
if (openSeq !== state.openSeq) {
clear();
resolve();
return;
}
try {
frame.contentWindow.addEventListener("afterprint", clear, { once: true });
frame.contentWindow.print();
resolve();
} catch {
fail();
}
};
frame.onerror = fail;
document.body.append(frame);
});
}
async function exportCurrentPage() {
if (!state.doc) {
if (!state.doc || state.outputting) {
return;
}
const format = el.pageExportFormat.value;
@@ -1321,7 +1385,7 @@ async function exportCurrentPage() {
const info = exportFormatInfo(format);
const label = info?.label || String(format || "").toUpperCase();
const openSeq = state.openSeq;
setExportControlsDisabled(true);
setOutputting(true);
setBusy(true, `正在准备 ${label}`, 18, STATUS.pageExporting);
try {
await waitForPaint();
@@ -1349,8 +1413,8 @@ async function exportCurrentPage() {
} finally {
if (openSeq === state.openSeq) {
setBusy(false);
updateControls();
}
setOutputting(false);
}
}
@@ -2182,7 +2246,7 @@ async function focusSignatureStamp(stamp) {
await renderPage(pageIndex, { fit: false, scroll: false });
await nextFrame();
highlightSignatureStamp(stamp);
setStatus(stamp.page ? `已定位签名外观 第 ${stamp.page} 页` : "已定位签名外观");
setStatus(stamp.page ? `已定位签名 第 ${stamp.page} 页` : "已定位签名");
}
function highlightSignatureStamp(stamp) {
@@ -2287,7 +2351,7 @@ function pageStatus(index, pageCount) {
function statusText(status) {
switch (status) {
case "pending":
return "待检查";
return "待查";
case "embedded":
return "内嵌";
case "matched":
@@ -2510,6 +2574,7 @@ function updateFitSpace() {
function updateControls() {
const hasDoc = Boolean(state.doc);
const canExport = hasDoc && !state.outputting;
const pageCount = state.doc ? state.doc.pageCount : 0;
el.prevButton.disabled = !hasDoc || state.pageIndex <= 0;
el.nextButton.disabled = !hasDoc || state.pageIndex >= pageCount - 1;
@@ -2524,23 +2589,22 @@ function updateControls() {
el.fitButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "width");
el.fitHeightButton.toggleAttribute("aria-pressed", hasDoc && state.fitMode === "height");
updateAnnotationButton();
el.pageExportFormat.disabled = !hasDoc || !state.exportFormats.length;
el.exportPageButton.disabled = !hasDoc || !state.exportFormats.length;
el.exportButton.disabled = !hasDoc;
el.pageExportFormat.disabled = !canExport || !state.exportFormats.length;
el.exportPageButton.disabled = !canExport || !state.exportFormats.length;
el.exportButton.disabled = !canExport;
el.printButton.disabled = !canExport;
updateDPIControl();
}
function updateAnnotationButton() {
el.annotationButton.setAttribute("aria-pressed", String(state.renderAnnotations));
el.annotationButton.title = state.renderAnnotations ? "关闭注解渲染" : "开启注解渲染";
el.annotationButton.title = state.renderAnnotations ? "关闭注解" : "开启注解";
el.annotationButton.setAttribute("aria-label", el.annotationButton.title);
}
function setExportControlsDisabled(disabled) {
el.exportButton.disabled = disabled;
el.exportPageButton.disabled = disabled;
el.pageExportFormat.disabled = disabled;
updateDPIControl(disabled);
function setOutputting(outputting) {
state.outputting = outputting;
updateControls();
}
function callWASM(name, ...args) {
@@ -2659,8 +2723,8 @@ function exportFormatUsesDPI(value) {
return value === "png" || value === "jpg";
}
function updateDPIControl(disabled = false) {
el.imageDPI.disabled = disabled || !state.doc || !exportFormatUsesDPI(el.pageExportFormat.value);
function updateDPIControl() {
el.imageDPI.disabled = state.outputting || !state.doc || !exportFormatUsesDPI(el.pageExportFormat.value);
}
function currentImageDPI() {