feat(签名展示): OFDGo WebUI支持签名展示
build.yaml / build (push) Successful in 35s

This commit is contained in:
2026-07-08 11:02:20 +08:00
parent 98b37d865a
commit 61da0f3f5d
8 changed files with 671 additions and 18 deletions
+11
View File
@@ -80,11 +80,22 @@
<dt>类型</dt>
<dd id="metaType">-</dd>
</div>
<div>
<dt>签名</dt>
<dd id="metaSignatures">0</dd>
</div>
<div>
<dt>字体</dt>
<dd id="metaFonts">0</dd>
</div>
</dl>
<section id="signaturePanel" class="signature-panel" aria-labelledby="signaturePanelTitle" hidden>
<div class="panel-row">
<div id="signaturePanelTitle" class="panel-title compact">签名</div>
<span id="signatureSummary" class="count-pill">0</span>
</div>
<div id="signatureList" class="signature-list"></div>
</section>
<section class="doc-font-panel" aria-labelledby="docFontPanelTitle">
<div class="panel-row">
<div id="docFontPanelTitle" class="panel-title compact">OFD 字体</div>
+118 -7
View File
@@ -519,6 +519,23 @@ body[data-hide-meta] .meta-panel {
display: none;
}
.stamp-highlight {
position: absolute;
z-index: 4;
border: 2px solid var(--accent);
background: rgba(15, 123, 104, 0.08);
box-shadow: 0 0 0 3px rgba(15, 123, 104, 0.16);
pointer-events: none;
animation: stampPulse 0.9s ease-in-out 2;
}
@keyframes stampPulse {
50% {
border-color: var(--accent-strong);
box-shadow: 0 0 0 5px rgba(15, 123, 104, 0.18);
}
}
.ofd-svg {
display: block;
max-width: none;
@@ -548,7 +565,8 @@ body[data-hide-meta] .meta-panel {
}
.font-panel,
.doc-font-panel {
.doc-font-panel,
.signature-panel {
margin-top: 18px;
padding-top: 12px;
border-top: 1px solid var(--line);
@@ -569,12 +587,14 @@ body[data-hide-meta] .meta-panel {
white-space: nowrap;
}
.doc-font-list {
.doc-font-list,
.signature-list {
display: grid;
gap: 8px;
}
.doc-font-row {
.doc-font-row,
.signature-row {
display: grid;
gap: 6px;
padding: 9px;
@@ -585,6 +605,7 @@ body[data-hide-meta] .meta-panel {
.page-list-item:not(:disabled):hover,
.doc-font-row:hover,
.signature-row:hover,
.font-row:hover {
border-color: var(--line-strong);
background: #ffffff;
@@ -600,14 +621,26 @@ body[data-hide-meta] .meta-panel {
background: #fffdf7;
}
.doc-font-head {
.signature-row.valid {
border-color: #bdd8cc;
background: #fbfefd;
}
.signature-row.invalid {
border-color: #e2c7c4;
background: #fffdfc;
}
.doc-font-head,
.signature-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 8px;
}
.doc-font-name {
.doc-font-name,
.signature-name {
overflow: hidden;
color: #1f2428;
font-size: 13px;
@@ -616,7 +649,51 @@ body[data-hide-meta] .meta-panel {
white-space: nowrap;
}
.doc-font-badges {
.signature-name-button {
display: inline;
padding: 0;
border: 0;
background: transparent;
color: inherit;
font: inherit;
font-weight: inherit;
cursor: pointer;
text-align: left;
}
.signature-name-button:not(:disabled):hover {
background: transparent;
color: var(--accent-strong);
text-decoration: underline;
}
.signature-name-with-stamps {
white-space: normal;
}
.signature-stamp-group {
color: var(--text-muted);
font-weight: 400;
}
.signature-stamp-link {
padding: 0;
border: 0;
background: transparent;
color: var(--accent-strong);
font: inherit;
font-weight: 400;
line-height: inherit;
cursor: pointer;
}
.signature-stamp-link:not(:disabled):hover {
background: transparent;
text-decoration: underline;
}
.doc-font-badges,
.signature-badges {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
@@ -647,11 +724,45 @@ body[data-hide-meta] .meta-panel {
color: #705719;
}
.font-badge.missing {
.font-badge.missing,
.font-badge.invalid {
background: #f9e8e5;
color: #803f38;
}
.font-badge.valid {
background: #e4f3ef;
color: var(--accent-strong);
}
.signature-line {
display: grid;
grid-template-columns: 30px minmax(0, 1fr);
align-items: start;
gap: 7px;
color: var(--text-muted);
font-size: 11px;
line-height: 1.35;
}
.signature-label {
color: #7b8791;
white-space: nowrap;
}
.signature-value {
min-width: 0;
overflow-wrap: anywhere;
}
.signature-line.ok .signature-value {
color: var(--accent-strong);
}
.signature-line.fail .signature-value {
color: #803f38;
}
.doc-font-detail {
color: var(--text-muted);
font-size: 11px;
+194
View File
@@ -107,6 +107,10 @@ const el = {
metaVersion: document.querySelector("#metaVersion"),
metaType: document.querySelector("#metaType"),
metaFonts: document.querySelector("#metaFonts"),
metaSignatures: document.querySelector("#metaSignatures"),
signaturePanel: document.querySelector("#signaturePanel"),
signatureSummary: document.querySelector("#signatureSummary"),
signatureList: document.querySelector("#signatureList"),
docFontList: document.querySelector("#docFontList"),
docFontSummary: document.querySelector("#docFontSummary"),
availableFontSummary: document.querySelector("#availableFontSummary"),
@@ -1500,12 +1504,201 @@ function renderMeta() {
el.metaVersion.textContent = doc.version || "-";
el.metaType.textContent = doc.docType || "-";
el.metaFonts.textContent = String(doc.fontCount || 0);
el.metaSignatures.textContent = String(doc.signatureCount || 0);
el.pageTotal.textContent = String(doc.pageCount || 0);
renderSignatures();
renderDocumentFonts();
renderFontList();
updateLocalFontButton();
}
function renderSignatures() {
const signatures = state.doc?.signatures || [];
const showPanel = Boolean(state.doc?.signatureError || signatures.length);
el.signaturePanel.hidden = !showPanel;
el.signatureList.replaceChildren();
el.signatureSummary.textContent = signatureSummary(signatures);
if (!showPanel) {
return;
}
if (state.doc?.signatureError) {
const empty = document.createElement("div");
empty.className = "font-empty";
empty.textContent = state.doc.signatureError;
el.signatureList.append(empty);
return;
}
const fragment = document.createDocumentFragment();
for (const signature of signatures) {
const row = document.createElement("div");
row.className = `signature-row ${signature.valid ? "valid" : "invalid"}`;
const head = document.createElement("div");
head.className = "signature-head";
const name = signatureNameNode(signature);
const badges = document.createElement("div");
badges.className = "signature-badges";
badges.append(fontBadge(signature.valid ? "有效" : "无效", signature.valid ? "valid" : "invalid"));
head.append(name, badges);
row.append(head);
appendSignatureLine(row, "签者", signature.signer);
appendSignatureLine(row, "时间", formatSignatureTime(signature.signatureDateTime));
appendSignatureLine(row, "机构", signatureAgency(signature));
appendSignatureLine(row, "版本", signature.version);
appendSignatureLine(row, "保护", `${signature.referencePassed || 0}/${signature.referenceCount || 0}`);
appendSignatureCheck(row, "原文", signature.dataHashOK);
appendSignatureCheck(row, "摘要", signature.digestOK);
appendSignatureCheck(row, "签名", signature.signedValueOK);
appendSignatureCheck(row, "证书", signature.certOK);
if (signature.type !== "Sign") {
appendSignatureCheck(row, "印章", signature.sealOK && signature.sealMatchOK);
appendSignatureCheck(row, "匹配", signature.sealMatchOK);
}
appendSignatureLine(row, "算法", signature.signatureMethod);
appendSignatureLine(row, "散列", signature.digestMethod);
appendSignatureLine(row, "主体", signature.signSubject && signature.signSubject !== signature.signer ? signature.signSubject : "");
appendSignatureLine(row, "颁发", signature.signIssuer);
appendSignatureLine(row, "序列", signature.signSerial);
appendSignatureLine(row, "章图", signature.sealType);
appendSignatureLine(row, "章证", signature.sealSubject);
appendSignatureLine(row, "编号", signature.id);
appendSignatureLine(row, "错误", signature.error, "fail");
fragment.append(row);
}
el.signatureList.append(fragment);
}
function signatureSummary(signatures) {
if (!signatures.length) {
return "0";
}
const invalid = signatures.filter((signature) => !signature.valid).length;
if (invalid) {
return `${signatures.length} · 异常 ${invalid}`;
}
return `通过 ${signatures.length}`;
}
function signatureName() {
return "签名";
}
function signatureNameNode(signature) {
const stamps = signature.stamps || [];
if (!stamps.length) {
const name = document.createElement("div");
name.className = "signature-name";
name.textContent = signatureName();
return name;
}
const name = document.createElement("div");
name.className = "signature-name signature-name-with-stamps";
const button = document.createElement("button");
button.type = "button";
button.className = "signature-name-button";
button.textContent = signatureName();
button.addEventListener("click", () => focusSignatureStamp(stamps[0]));
name.append(button, signatureStampGroup(stamps));
return name;
}
function signatureStampGroup(stamps) {
const group = document.createElement("span");
group.className = "signature-stamp-group";
group.append("");
for (const [index, stamp] of stamps.entries()) {
if (index > 0) {
group.append("、");
}
const button = document.createElement("button");
button.type = "button";
button.className = "signature-stamp-link";
button.textContent = `${index + 1}`;
button.addEventListener("click", () => focusSignatureStamp(stamp));
group.append(button);
}
group.append("");
return group;
}
function signatureAgency(signature) {
if (signature.company && signature.provider) {
return `${signature.company} · ${signature.provider}`;
}
return signature.company || signature.provider || "";
}
function formatSignatureTime(value) {
const text = String(value || "").trim();
const digits = text.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/);
if (digits) {
return `${digits[1]}-${digits[2]}-${digits[3]} ${digits[4]}:${digits[5]}:${digits[6]}`;
}
return text.replace("T", " ").replace(/(?:Z|[+-]\d{2}:?\d{2})$/, "");
}
function appendSignatureLine(row, label, value, status = "") {
if (!value && value !== 0) {
return;
}
const line = document.createElement("div");
line.className = status ? `signature-line ${status}` : "signature-line";
const key = document.createElement("span");
key.className = "signature-label";
key.textContent = label;
const text = document.createElement("span");
text.className = "signature-value";
text.textContent = String(value);
line.append(key, text);
row.append(line);
}
function appendSignatureCheck(row, label, ok) {
appendSignatureLine(row, label, ok ? "通过" : "失败", ok ? "ok" : "fail");
}
async function focusSignatureStamp(stamp) {
const pageIndex = (stamp.page || 0) - 1;
if (!state.doc || pageIndex < 0) {
return;
}
await renderPage(pageIndex);
await nextFrame();
highlightSignatureStamp(stamp);
setStatus(stamp.page ? `已定位签名外观 第 ${stamp.page}` : "已定位签名外观");
}
function highlightSignatureStamp(stamp) {
clearStampHighlights();
const pageIndex = (stamp.page || 0) - 1;
const shell = pageShell(pageIndex);
if (!shell || !stamp.width || !stamp.height) {
return;
}
const mark = document.createElement("div");
mark.className = "stamp-highlight";
mark.style.left = `${stamp.x * MM_TO_PX * state.scale}px`;
mark.style.top = `${stamp.y * MM_TO_PX * state.scale}px`;
mark.style.width = `${stamp.width * MM_TO_PX * state.scale}px`;
mark.style.height = `${stamp.height * MM_TO_PX * state.scale}px`;
shell.append(mark);
mark.scrollIntoView({ block: "center", inline: "center" });
window.setTimeout(() => mark.remove(), 1800);
}
function clearStampHighlights() {
for (const mark of el.svgHost.querySelectorAll(".stamp-highlight")) {
mark.remove();
}
}
function renderDocumentFonts() {
const fonts = state.doc?.fonts || [];
el.docFontList.replaceChildren();
@@ -1654,6 +1847,7 @@ function applyFit(updateStatus = true) {
function setScale(nextScale, updateStatus = true, fitMode = "free") {
state.fitMode = fitMode;
state.scale = Math.min(4, Math.max(0.2, nextScale));
clearStampHighlights();
layoutPages();
el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`;
if (updateStatus && state.doc) {