feat(资源管理): 统一附件管理并清理重复入口
build.yaml / build (push) Successful in 50s

This commit is contained in:
xiaoqidun committed 2026-10-13 13:04:37 +08:00
1 parent ab0c6dfed0
commit 656a0d51b0
3 files changed
+24 -130

No files matched your search

+2 -5
View File
@@ -367,6 +367,7 @@
<button id="resourceAdd" class="button" type="button" title="添加资源" aria-label="添加资源">添加</button> <button id="resourceAdd" class="button" type="button" title="添加资源" aria-label="添加资源">添加</button>
<button id="resourcePreview" class="button" type="button">预览</button> <button id="resourcePreview" class="button" type="button">预览</button>
<button id="resourceExport" class="button" type="button">导出</button> <button id="resourceExport" class="button" type="button">导出</button>
<button id="resourceRename" class="button" type="button" hidden>改名</button>
<button id="resourceReplace" class="button" type="button">替换</button> <button id="resourceReplace" class="button" type="button">替换</button>
<button id="resourceDelete" class="button" type="button">删除</button> <button id="resourceDelete" class="button" type="button">删除</button>
<button id="resourceLocate" class="button" type="button">定位</button> <button id="resourceLocate" class="button" type="button">定位</button>
@@ -744,16 +745,12 @@
</dl> </dl>
<details id="conversionNotice" class="conversion-notice" hidden> <details id="conversionNotice" class="conversion-notice" hidden>
<summary>提示</summary> <summary>提示</summary>
<div id="conversionWarnings" class="attachment-detail"></div> <div id="conversionWarnings" class="panel-detail"></div>
</details> </details>
<section id="securityPanel" class="security-panel" aria-labelledby="securityPanelTitle" hidden> <section id="securityPanel" class="security-panel" aria-labelledby="securityPanelTitle" hidden>
<div class="panel-row"><div id="securityPanelTitle" class="panel-title compact">安全</div><div class="panel-actions"><button id="encryptSaveButton" class="small-button" type="button" title="加密文档" aria-label="加密文档" aria-haspopup="dialog" aria-controls="encryptionPanel" disabled>加密</button><button id="signButton" class="small-button" type="button" aria-haspopup="dialog" aria-controls="signPanel" disabled>签章</button><button id="verifyButton" class="small-button" type="button" aria-haspopup="dialog" aria-controls="verifyPanel" disabled>验签</button></div></div> <div class="panel-row"><div id="securityPanelTitle" class="panel-title compact">安全</div><div class="panel-actions"><button id="encryptSaveButton" class="small-button" type="button" title="加密文档" aria-label="加密文档" aria-haspopup="dialog" aria-controls="encryptionPanel" disabled>加密</button><button id="signButton" class="small-button" type="button" aria-haspopup="dialog" aria-controls="signPanel" disabled>签章</button><button id="verifyButton" class="small-button" type="button" aria-haspopup="dialog" aria-controls="verifyPanel" disabled>验签</button></div></div>
<dl class="meta-list"><div><dt>加密</dt><dd id="securityEncryption">未加密</dd></div><div id="securitySchemeRow" hidden><dt>方式</dt><dd id="securityScheme"></dd></div><div id="securityMethodRow" hidden><dt>算法</dt><dd id="securityMethod"></dd></div></dl> <dl class="meta-list"><div><dt>加密</dt><dd id="securityEncryption">未加密</dd></div><div id="securitySchemeRow" hidden><dt>方式</dt><dd id="securityScheme"></dd></div><div id="securityMethodRow" hidden><dt>算法</dt><dd id="securityMethod"></dd></div></dl>
</section> </section>
<section id="attachmentPanel" class="attachment-panel" aria-labelledby="attachmentPanelTitle" hidden>
<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> <section id="structurePanel" class="structure-panel" aria-labelledby="structurePanelTitle" hidden>
<div class="panel-row"><div id="structurePanelTitle" class="panel-title compact">结构</div><div class="panel-actions"><span id="structureSummary" class="count-pill">0</span><button id="structureManage" class="small-button" type="button" title="管理标引" aria-haspopup="dialog" aria-controls="structureDialog" hidden>管理</button></div></div> <div class="panel-row"><div id="structurePanelTitle" class="panel-title compact">结构</div><div class="panel-actions"><span id="structureSummary" class="count-pill">0</span><button id="structureManage" class="small-button" type="button" title="管理标引" aria-haspopup="dialog" aria-controls="structureDialog" hidden>管理</button></div></div>
<div id="structureList" class="structure-list"></div> <div id="structureList" class="structure-list"></div>
+1 -42
View File
@@ -2838,7 +2838,6 @@ audio.page-media {
.font-panel, .font-panel,
.doc-font-panel, .doc-font-panel,
.attachment-panel,
.signature-panel, .signature-panel,
.structure-panel, .structure-panel,
.security-panel { .security-panel {
@@ -2847,46 +2846,6 @@ audio.page-media {
border-top: 1px solid var(--line); border-top: 1px solid var(--line);
} }
.attachment-list {
display: grid;
gap: 8px;
}
.attachment-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
}
.attachment-row .attachment-item {
min-width: 0;
}
.attachment-actions {
flex-wrap: nowrap;
}
.attachment-item {
width: 100%;
padding: 4px 0;
border: 0;
background: transparent;
color: var(--accent-strong);
font-size: 13px;
line-height: 1.4;
text-align: left;
overflow-wrap: anywhere;
}
.attachment-item:not(:disabled):hover {
background: transparent;
}
.attachment-item:hover .attachment-name {
text-decoration: underline;
}
.conversion-notice { .conversion-notice {
margin-block: 12px; margin-block: 12px;
} }
@@ -2900,7 +2859,7 @@ audio.page-media {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.attachment-detail { .panel-detail {
color: var(--text-muted); color: var(--text-muted);
font-size: 12px; font-size: 12px;
} }
+21 -83
View File
@@ -23,7 +23,7 @@ const batch = { items: [], formats: [], running: false, canceled: false, worker:
const batchElements = Object.fromEntries(["Button", "Panel", "Form", "Input", "Add", "Clear", "Count", "Format", "Compression", "FontRow", "FontMode", "Destination", "DPIRow", "DPI", "List", "Empty", "Progress", "Status", "Close", "Cancel", "Start"].map(name => [name, document.querySelector(`#convert${name}`)])); const batchElements = Object.fromEntries(["Button", "Panel", "Form", "Input", "Add", "Clear", "Count", "Format", "Compression", "FontRow", "FontMode", "Destination", "DPIRow", "DPI", "List", "Empty", "Progress", "Status", "Close", "Cancel", "Start"].map(name => [name, document.querySelector(`#convert${name}`)]));
const fontBinding = { font: null, choices: [], sequence: 0 }; const fontBinding = { font: null, choices: [], sequence: 0 };
const fontBindingElements = Object.fromEntries(["Panel", "Form", "Title", "Name", "SourceRow", "Source", "File", "Hint", "Status", "Cancel", "Submit"].map(name => [name, document.querySelector(`#fontBinding${name}`)])); const fontBindingElements = Object.fromEntries(["Panel", "Form", "Title", "Name", "SourceRow", "Source", "File", "Hint", "Status", "Cancel", "Submit"].map(name => [name, document.querySelector(`#fontBinding${name}`)]));
const resourceElements = Object.fromEntries(["ManageButton", "Panel", "Form", "Kind", "List", "Empty", "Reference", "Add", "Preview", "Export", "Replace", "Delete", "Locate", "Close", "Binding", "Page", "Object", "Bind", "Unbind", "Viewer", "Status", "File"].map(name => [name, document.querySelector(`#resource${name}`)])); const resourceElements = Object.fromEntries(["ManageButton", "Panel", "Form", "Kind", "List", "Empty", "Reference", "Add", "Preview", "Export", "Rename", "Replace", "Delete", "Locate", "Close", "Binding", "Page", "Object", "Bind", "Unbind", "Viewer", "Status", "File"].map(name => [name, document.querySelector(`#resource${name}`)]));
const resourceManager = { items: [], selected: null, busy: false, sequence: 0, url: null, font: null, fileAction: null }; const resourceManager = { items: [], selected: null, busy: false, sequence: 0, url: null, font: null, fileAction: null };
let wasmPromise = null; let wasmPromise = null;
@@ -525,9 +525,6 @@ const el = {
metaVersion: document.querySelector("#metaVersion"), metaVersion: document.querySelector("#metaVersion"),
metaSignatures: document.querySelector("#metaSignatures"), metaSignatures: document.querySelector("#metaSignatures"),
metaFonts: document.querySelector("#metaFonts"), metaFonts: document.querySelector("#metaFonts"),
attachmentPanel: document.querySelector("#attachmentPanel"),
attachmentList: document.querySelector("#attachmentList"),
attachmentAdd: document.querySelector("#attachmentAdd"),
attachmentDialog: document.querySelector("#attachmentDialog"), attachmentDialog: document.querySelector("#attachmentDialog"),
attachmentForm: document.querySelector("#attachmentForm"), attachmentForm: document.querySelector("#attachmentForm"),
attachmentTitle: document.querySelector("#attachmentTitle"), attachmentTitle: document.querySelector("#attachmentTitle"),
@@ -769,9 +766,15 @@ resourceElements.Replace.addEventListener("click", async () => {
if (!fontBindingElements.Panel.open) await resourceTask(loadManagedResources); if (!fontBindingElements.Panel.open) await resourceTask(loadManagedResources);
} else chooseResourceFile("replace"); } else chooseResourceFile("replace");
}); });
resourceElements.Rename.addEventListener("click", () => {
const item = resourceManager.selected;
if (item?.kind === "Attachment") openAttachmentDialog("rename", item);
});
resourceElements.Delete.addEventListener("click", () => resourceTask(async () => { resourceElements.Delete.addEventListener("click", () => resourceTask(async () => {
const item = resourceManager.selected; const item = resourceManager.selected;
if (item && window.confirm(`删除资源“${item.name}”?`)) await changeManagedResource("delete", item); if (!item) return;
const message = item.kind === "Attachment" ? `删除附件“${item.name}”?相关附件链接将一并移除。` : `删除资源“${item.name}”?`;
if (window.confirm(message)) await changeManagedResource("delete", item);
})); }));
for (const [button, action] of [[resourceElements.Bind, "bind"], [resourceElements.Unbind, "unbind"]]) { for (const [button, action] of [[resourceElements.Bind, "bind"], [resourceElements.Unbind, "unbind"]]) {
button.addEventListener("click", () => resourceTask(() => changeManagedResource(action, resourceManager.selected))); button.addEventListener("click", () => resourceTask(() => changeManagedResource(action, resourceManager.selected)));
@@ -5157,14 +5160,16 @@ function updateResourceControls() {
const disabled = resourceManager.busy || !state.doc || document.body.hasAttribute("aria-busy"); const disabled = resourceManager.busy || !state.doc || document.body.hasAttribute("aria-busy");
const media = item && ["Audio", "Video"].includes(item.kind); const media = item && ["Audio", "Video"].includes(item.kind);
resourceElements.Add.hidden = resourceElements.Replace.hidden = resourceElements.Delete.hidden = !editing; resourceElements.Add.hidden = resourceElements.Replace.hidden = resourceElements.Delete.hidden = !editing;
resourceElements.Rename.hidden = !editing || item?.kind !== "Attachment";
resourceElements.Rename.disabled = disabled || item?.kind !== "Attachment";
resourceElements.Add.disabled = disabled; resourceElements.Add.disabled = disabled;
resourceElements.Reference.disabled = disabled || !item?.references?.length; resourceElements.Reference.disabled = disabled || !item?.references?.length;
resourceElements.Preview.disabled = disabled || !item?.file || item.missing || item.kind === "Attachment"; resourceElements.Preview.disabled = disabled || !item?.file || item.missing || item.kind === "Attachment";
resourceElements.Export.disabled = disabled || !item?.file || item.missing; resourceElements.Export.disabled = disabled || !item?.file || item.missing;
resourceElements.Replace.disabled = disabled || !item || !item.complete; resourceElements.Replace.disabled = disabled || !item || item.kind !== "Attachment" && !item.complete;
resourceElements.Replace.textContent = item?.kind === "Font" ? item.file ? "外置" : "嵌入" : "替换"; resourceElements.Replace.textContent = item?.kind === "Font" ? item.file ? "外置" : "嵌入" : "替换";
resourceElements.Delete.disabled = disabled || !item?.complete || Boolean(item?.references?.length); resourceElements.Delete.disabled = disabled || !item || item.kind !== "Attachment" && (!item.complete || Boolean(item.references?.length));
resourceElements.Delete.title = item?.references?.length ? "资源仍被引用" : "删除资源"; resourceElements.Delete.title = item?.kind === "Attachment" ? "删除附件" : item?.references?.length ? "资源仍被引用" : "删除资源";
resourceElements.Locate.disabled = disabled || !(item?.references?.[Number(resourceElements.Reference.value)]?.page >= 0); resourceElements.Locate.disabled = disabled || !(item?.references?.[Number(resourceElements.Reference.value)]?.page >= 0);
resourceElements.Binding.hidden = !editing || !media; resourceElements.Binding.hidden = !editing || !media;
resourceElements.Bind.disabled = resourceElements.Unbind.disabled = disabled || !media || !resourceElements.Object.value; resourceElements.Bind.disabled = resourceElements.Unbind.disabled = disabled || !media || !resourceElements.Object.value;
@@ -5193,7 +5198,10 @@ async function changeManagedResource(action, item, file) {
if (!item || !state.editing) return; if (!item || !state.editing) return;
releaseResourcePreview(); releaseResourcePreview();
const format = file?.name.includes(".") ? file.name.split(".").at(-1).toUpperCase() : ""; const format = file?.name.includes(".") ? file.name.split(".").at(-1).toUpperCase() : "";
if (await changeDocument("ofdgoChangeResource", null, action, item.kind, item.id, format, file || null, Number(resourceElements.Page.value), resourceElements.Object.value)) { const changed = item.kind === "Attachment"
? await changeDocument("ofdgoChangeAttachment", null, action, item.id, file ? [file.name] : [], file ? [file] : [])
: await changeDocument("ofdgoChangeResource", null, action, item.kind, item.id, format, file || null, Number(resourceElements.Page.value), resourceElements.Object.value);
if (changed) {
await loadManagedResources(); await loadManagedResources();
await loadResourceTargets(); await loadResourceTargets();
} }
@@ -5258,7 +5266,6 @@ function downloadAttachment(attachment) {
return downloadResource(attachment, "ofdgoExportAttachment", "附件"); return downloadResource(attachment, "ofdgoExportAttachment", "附件");
} }
editorClick(el.attachmentAdd, () => openAttachmentDialog("add"));
el.attachmentCancel.addEventListener("click", () => el.attachmentDialog.close()); el.attachmentCancel.addEventListener("click", () => el.attachmentDialog.close());
el.attachmentDialog.addEventListener("cancel", event => { if (el.attachmentForm.inert) event.preventDefault(); }); el.attachmentDialog.addEventListener("cancel", event => { if (el.attachmentForm.inert) event.preventDefault(); });
el.attachmentDialog.addEventListener("close", () => { el.attachmentDialog.addEventListener("close", () => {
@@ -5273,7 +5280,7 @@ el.attachmentForm.addEventListener("submit", async event => {
const names = edit.action === "rename" ? [el.attachmentName.value] : files.map(file => file.name); const names = edit.action === "rename" ? [el.attachmentName.value] : files.map(file => file.name);
if (await changeDocument("ofdgoChangeAttachment", null, edit.action, edit.id, names, files)) { if (await changeDocument("ofdgoChangeAttachment", null, edit.action, edit.id, names, files)) {
el.attachmentDialog.close(); el.attachmentDialog.close();
setStatus({add:"附件已添加", rename:"附件已改名", replace:"附件已替换"}[edit.action]); setStatus(edit.action === "rename" ? "附件已改名" : "附件已添加");
} }
}); });
@@ -5282,11 +5289,10 @@ function openAttachmentDialog(action, attachment) {
const rename = action === "rename"; const rename = action === "rename";
el.attachmentForm.reset(); el.attachmentForm.reset();
el.attachmentDialog.attachmentEdit = {action, id: attachment?.id || "", openSeq: state.openSeq}; el.attachmentDialog.attachmentEdit = {action, id: attachment?.id || "", openSeq: state.openSeq};
el.attachmentTitle.textContent = {add:"添加附件", rename:"附件改名", replace:"替换附件"}[action]; el.attachmentTitle.textContent = rename ? "附件改名" : "添加附件";
el.attachmentSubmit.textContent = {add:"添加", rename:"确定", replace:"替换"}[action]; el.attachmentSubmit.textContent = rename ? "确定" : "添加";
el.attachmentFileRow.hidden = rename; el.attachmentFileRow.hidden = rename;
el.attachmentFile.required = !rename; el.attachmentFile.required = !rename;
el.attachmentFile.multiple = action === "add";
el.attachmentNameRow.hidden = !rename; el.attachmentNameRow.hidden = !rename;
el.attachmentName.required = rename; el.attachmentName.required = rename;
el.attachmentName.value = attachment?.name || ""; el.attachmentName.value = attachment?.name || "";
@@ -8442,7 +8448,6 @@ function renderMeta(keepDetails = false) {
el.pageTotal.textContent = String(doc.pageCount || 0); el.pageTotal.textContent = String(doc.pageCount || 0);
if (keepDetails && doc.detailsPending) return; if (keepDetails && doc.detailsPending) return;
el.metaSignatures.textContent = doc.detailsPending ? "正在检查签名" : doc.detailsError ? "读取失败" : String(doc.signatureCount || 0); 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.signatureList, [doc.signatures, doc.signatureError], renderSignatures);
updateStructureControls(); updateStructureControls();
renderMetaContent(el.docFontList, doc.fonts || [], renderDocumentFonts); renderMetaContent(el.docFontList, doc.fonts || [], renderDocumentFonts);
@@ -8457,70 +8462,6 @@ function renderMetaContent(node, value, render) {
metaContents.set(node, key); metaContents.set(node, key);
} }
function renderAttachments() {
const attachments = (state.doc?.attachments || []).filter(item => state.editing || item.visible !== false);
el.attachmentPanel.hidden = !state.doc || !state.editing && !attachments.length && !state.doc.attachmentError;
el.attachmentAdd.hidden = !state.editing;
el.attachmentList.replaceChildren();
if (state.doc?.attachmentError) {
const error = document.createElement("div");
error.className = "attachment-detail";
error.textContent = "附件读取失败";
error.title = state.doc.attachmentError;
el.attachmentList.append(error);
return;
}
if (!attachments.length) {
const empty = document.createElement("div");
empty.className = "attachment-detail";
empty.textContent = "暂无附件";
el.attachmentList.append(empty);
}
for (const attachment of attachments) {
const row = document.createElement("div");
row.className = "attachment-row";
const item = document.createElement("button");
item.type = "button";
item.className = "attachment-item";
item.title = `另存 ${attachment.fileName}`;
item.setAttribute("aria-label", `另存 ${attachment.name}`);
const name = document.createElement("span");
name.className = "attachment-name";
name.textContent = attachment.name;
item.append(name);
const detail = [attachment.format, attachment.size == null ? "" : formatBytes(attachment.size * 1024), attachment.visible === false ? "隐藏" : ""].filter(Boolean).join(" · ");
if (detail) {
const meta = document.createElement("span");
meta.className = "attachment-detail";
meta.textContent = ` · ${detail}`;
item.append(meta);
}
item.addEventListener("click", () => downloadAttachment(attachment));
row.append(item);
if (state.editing) {
const actions = document.createElement("div");
actions.className = "panel-actions attachment-actions";
actions.setAttribute("role", "group");
actions.setAttribute("aria-label", `${attachment.name} 操作`);
for (const [action, text] of [["rename", "改名"], ["replace", "替换"], ["delete", "删除"]]) {
const button = document.createElement("button");
button.type = "button";
button.className = "small-button";
button.textContent = text;
button.title = `${text}附件`;
editorClick(button, async () => {
if (action === "delete") {
if (window.confirm(`删除附件“${attachment.name}”?`) && await changeDocument("ofdgoChangeAttachment", null, action, attachment.id, [], [])) setStatus("附件已删除");
} else openAttachmentDialog(action, attachment);
});
actions.append(button);
}
row.append(actions);
}
el.attachmentList.append(row);
}
}
function openStructureDialog() { function openStructureDialog() {
if (!state.editing || !state.doc || document.body.hasAttribute("aria-busy")) return; if (!state.editing || !state.doc || document.body.hasAttribute("aria-busy")) return;
el.structureEntry.replaceChildren(); el.structureEntry.replaceChildren();
@@ -8702,7 +8643,7 @@ function structureNode(node, index, path) {
} }
if (!references) return; if (!references) return;
const pending = document.createElement("div"); const pending = document.createElement("div");
pending.className = "attachment-detail"; pending.className = "panel-detail";
pending.textContent = "读取中"; pending.textContent = "读取中";
pending.setAttribute("role", "status"); pending.setAttribute("role", "status");
content.append(pending); content.append(pending);
@@ -9527,8 +9468,6 @@ function updateDocumentControls() {
function updateControls() { function updateControls() {
renderSecurity(); renderSecurity();
updateStructureControls(); updateStructureControls();
el.attachmentAdd.disabled = !state.editing || !state.ready || state.exporting;
renderMetaContent(el.attachmentList, [state.doc?.attachments, state.doc?.attachmentError, state.editing], renderAttachments);
const hasDoc = Boolean(state.doc); const hasDoc = Boolean(state.doc);
updateDocumentControls(); updateDocumentControls();
updateDisplayControls(); updateDisplayControls();
@@ -9747,7 +9686,6 @@ function setBusy(busy, text = "", percent = 0, status = "") {
el.documentForm.inert = busy; el.documentForm.inert = busy;
el.versionForm.inert = busy; el.versionForm.inert = busy;
el.attachmentForm.inert = busy; el.attachmentForm.inert = busy;
el.attachmentPanel.inert = busy;
el.annotationForm.inert = busy; el.annotationForm.inert = busy;
el.importForm.inert = busy && !state.importing; el.importForm.inert = busy && !state.importing;
for (const input of [el.importFile, el.importRange, el.importPosition, el.importOutlines]) input.disabled = busy; for (const input of [el.importFile, el.importRange, el.importPosition, el.importOutlines]) input.disabled = busy;