feat(电子公文): 支持标引解析与原文定位
build.yaml / build (push) Successful in 49s

This commit is contained in:
xiaoqidun committed 2026-10-13 00:30:27 +08:00
1 parent 3780a25140
commit e86fc6a4e0
10 files changed
+823 -8

No files matched your search

+4
View File
@@ -704,6 +704,10 @@
<div class="panel-row"><div id="attachmentPanelTitle" class="panel-title compact">附件</div><div class="panel-actions"><button id="attachmentAdd" class="small-button" type="button" title="添加附件" aria-haspopup="dialog" aria-controls="attachmentDialog" hidden>添加</button></div></div>
<div id="attachmentList" class="attachment-list"></div>
</section>
<section id="structurePanel" class="structure-panel" aria-labelledby="structurePanelTitle" hidden>
<div class="panel-row"><div id="structurePanelTitle" class="panel-title compact">结构</div><span id="structureSummary" class="count-pill">0</span></div>
<div id="structureList" class="structure-list"></div>
</section>
<section id="signaturePanel" class="signature-panel" aria-labelledby="signaturePanelTitle" hidden>
<div class="panel-row">
<div id="signaturePanelTitle" class="panel-title compact">签名</div>
+38
View File
@@ -2696,6 +2696,7 @@ audio.page-media {
.doc-font-panel,
.attachment-panel,
.signature-panel,
.structure-panel,
.security-panel {
margin-top: 18px;
padding-top: 12px;
@@ -2999,6 +3000,43 @@ audio.page-media {
overflow-wrap: anywhere;
}
.structure-list,
.structure-content {
display: grid;
gap: 6px;
min-width: 0;
}
.structure-node {
min-width: 0;
font-size: 12px;
}
.structure-node > summary {
cursor: pointer;
color: var(--accent-strong);
overflow-wrap: anywhere;
}
.structure-content {
margin-top: 6px;
}
.structure-content > .structure-node {
padding-left: 10px;
}
.structure-content .info-label {
white-space: normal;
overflow-wrap: anywhere;
}
.structure-links {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.info-line.ok .info-value {
color: var(--accent-strong);
}
+85
View File
@@ -537,6 +537,9 @@ const el = {
attachmentCancel: document.querySelector("#attachmentCancel"),
attachmentSubmit: document.querySelector("#attachmentSubmit"),
signaturePanel: document.querySelector("#signaturePanel"),
structurePanel: document.querySelector("#structurePanel"),
structureList: document.querySelector("#structureList"),
structureSummary: document.querySelector("#structureSummary"),
signatureSummary: document.querySelector("#signatureSummary"),
signatureList: document.querySelector("#signatureList"),
annotationNote: document.querySelector("#annotationNote"),
@@ -8147,6 +8150,7 @@ function renderMeta(keepDetails = false) {
el.metaSignatures.textContent = doc.detailsPending ? "正在检查签名" : doc.detailsError ? "读取失败" : String(doc.signatureCount || 0);
renderMetaContent(el.attachmentList, [doc.attachments, doc.attachmentError, state.editing], renderAttachments);
renderMetaContent(el.signatureList, [doc.signatures, doc.signatureError], renderSignatures);
renderMetaContent(el.structureList, [doc.structures, doc.structureError, doc.documentIndex, doc.documentVersion, state.openSeq, state.editorInfo?.revision], renderStructures);
renderMetaContent(el.docFontList, doc.fonts || [], renderDocumentFonts);
refreshEditorFonts();
updateLocalFontButton();
@@ -8223,6 +8227,87 @@ function renderAttachments() {
}
}
function renderStructures() {
const structures = state.doc?.structures || [];
el.structurePanel.hidden = !structures.length && !state.doc?.structureError;
el.structureSummary.textContent = structures.length;
el.structureList.replaceChildren();
if (state.doc?.structureError) appendInfoLine(el.structureList, "错误", state.doc.structureError, "fail");
for (const [index, structure] of structures.entries()) {
if (structure.root) el.structureList.append(structureNode(structure.root, index, []));
if (structure.error) appendInfoLine(el.structureList, "错误", structure.error, "fail");
}
}
function structureNode(node, index, path) {
const details = document.createElement("details");
details.className = "structure-node";
const summary = document.createElement("summary");
summary.textContent = node.name;
details.append(summary);
let loaded = false;
details.addEventListener("toggle", async () => {
if (!details.open || loaded) return;
loaded = true;
const content = document.createElement("div");
content.className = "structure-content";
details.append(content);
for (const attr of node.attributes || []) appendInfoLine(content, attr.name, attr.value);
if (node.text && !node.references?.length) appendInfoLine(content, "原文", node.text);
const children = node.children || [];
const references = node.references?.length || children.some(child => child.references?.length);
for (const [i, child] of children.entries()) {
if (!child.references?.length) content.append(structureNode(child, index, [...path, i]));
}
if (!references) return;
const pending = document.createElement("div");
pending.className = "attachment-detail";
pending.textContent = "读取中";
pending.setAttribute("role", "status");
content.append(pending);
const sequence = state.openSeq;
try {
const targets = await callWASM("ofdgoStructureTargets", index, path);
if (sequence !== state.openSeq || !details.isConnected) return;
pending.remove();
appendInfoLine(content, "原文", targets.map(target => target.text || "").join(""));
const pages = new Map();
for (const target of targets) {
if (target.error) { appendInfoLine(content, "引用错误", `${target.pageId} · ${target.id}:${target.error}`, "fail"); continue; }
const b = target.bounds;
if (!(b.W > 0 && b.H > 0)) continue;
const region = pages.get(target.page);
if (region) {
const right = Math.max(region.x + region.width, b.X + b.W), bottom = Math.max(region.y + region.height, b.Y + b.H);
region.x = Math.min(region.x, b.X); region.y = Math.min(region.y, b.Y);
region.width = right - region.x; region.height = bottom - region.y;
} else pages.set(target.page, { page: target.page, x: b.X, y: b.Y, width: b.W, height: b.H });
}
if (pages.size) {
const line = document.createElement("div");
line.className = "info-line";
const label = document.createElement("span");
label.className = "info-label"; label.textContent = "位置";
const links = document.createElement("span"); links.className = "info-value structure-links";
for (const region of pages.values()) {
const button = document.createElement("button");
button.type = "button"; button.className = "signature-stamp-link";
button.textContent = `第${region.page}页`; button.title = "定位原文";
button.addEventListener("click", () => focusPageRegion(region, node.name));
links.append(button);
}
line.append(label, links); content.append(line);
}
} catch (err) {
pending.textContent = "读取失败";
appendInfoLine(content, "错误", err.message, "fail");
loaded = false;
details.addEventListener("toggle", () => { if (!details.open) content.remove(); }, { once: true });
}
});
return details;
}
function renderSignatures() {
const signatures = state.doc?.signatures || [];
const showPanel = Boolean(state.doc?.signatureError || signatures.length);