feat(附件管理): 完善附件管理功能

This commit is contained in:
xiaoqidun committed 2026-09-30 02:22:56 +08:00
1 parent 418a2fa0a7
commit 38004171cf
10 files changed
+254 -42

No files matched your search

+13 -1
View File
@@ -489,6 +489,18 @@
<div class="form-actions"><div id="credentialsStatus" class="form-status" role="status" aria-live="polite"></div><button id="credentialsCancel" class="button" type="button">取消</button><button class="button" type="submit">解锁</button></div>
</form>
</dialog>
<dialog id="attachmentDialog" class="form-dialog" aria-labelledby="attachmentTitle">
<form id="attachmentForm">
<h2 id="attachmentTitle" class="form-title">添加附件</h2>
<div id="attachmentFileRow" class="form-row"><label for="attachmentFile">文件</label><input id="attachmentFile" type="file" multiple required></div>
<div id="attachmentNameRow" class="form-row" hidden><label for="attachmentName">名称</label><input id="attachmentName" type="text" autocomplete="off" spellcheck="false"></div>
<div class="form-actions">
<div id="attachmentStatus" class="form-status" role="status"></div>
<button id="attachmentCancel" class="button" type="button">取消</button>
<button id="attachmentSubmit" class="button" type="submit">添加</button>
</div>
</form>
</dialog>
<dialog id="encryptionPanel" class="form-dialog" aria-labelledby="encryptionTitle">
<form id="encryptionForm">
<h2 id="encryptionTitle" class="form-title">加密文档</h2>
@@ -640,7 +652,7 @@
<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 id="attachmentPanel" class="attachment-panel" aria-labelledby="attachmentPanelTitle" hidden>
<div id="attachmentPanelTitle" class="panel-title">附件</div>
<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="signaturePanel" class="signature-panel" aria-labelledby="signaturePanelTitle" hidden>
+21
View File
@@ -1904,6 +1904,7 @@ button.outline-link:hover {
.empty-state[hidden],
.editor-group > [hidden],
.panel-row > [hidden],
.panel-actions > [hidden],
.meta-list > [hidden],
.navigation-tabs[hidden],
.page-selection-tools[hidden],
@@ -2549,6 +2550,26 @@ button.outline-link:hover {
gap: 8px;
}
.attachment-row {
display: flex;
align-items: center;
gap: 8px;
}
.attachment-row .attachment-item {
min-width: 0;
flex: 1;
}
.attachment-actions {
flex: 0 0 60px;
width: 60px;
border: 1px solid var(--line-strong);
background: #ffffff;
color: #1f2428;
cursor: pointer;
}
.attachment-item {
width: 100%;
padding: 4px 0;
+93 -7
View File
@@ -481,6 +481,17 @@ const el = {
metaFonts: document.querySelector("#metaFonts"),
attachmentPanel: document.querySelector("#attachmentPanel"),
attachmentList: document.querySelector("#attachmentList"),
attachmentAdd: document.querySelector("#attachmentAdd"),
attachmentDialog: document.querySelector("#attachmentDialog"),
attachmentForm: document.querySelector("#attachmentForm"),
attachmentTitle: document.querySelector("#attachmentTitle"),
attachmentFile: document.querySelector("#attachmentFile"),
attachmentFileRow: document.querySelector("#attachmentFileRow"),
attachmentName: document.querySelector("#attachmentName"),
attachmentNameRow: document.querySelector("#attachmentNameRow"),
attachmentStatus: document.querySelector("#attachmentStatus"),
attachmentCancel: document.querySelector("#attachmentCancel"),
attachmentSubmit: document.querySelector("#attachmentSubmit"),
signaturePanel: document.querySelector("#signaturePanel"),
signatureSummary: document.querySelector("#signatureSummary"),
signatureList: document.querySelector("#signatureList"),
@@ -2916,6 +2927,8 @@ async function changeDocument(name, item, ...args) {
el.outlineStatus.textContent = err.message;
} else if (el.infoPanel.open) {
el.infoStatus.textContent = err.message;
} else if (el.attachmentDialog.open) {
el.attachmentStatus.textContent = err.message;
} else if (el.pagePanel.open) {
el.pageStatus.textContent = err.message;
} else if (el.paragraphPanel.open) {
@@ -4380,6 +4393,43 @@ function downloadAttachment(attachment) {
return downloadResource(attachment, "ofdgoExportAttachment", "附件");
}
editorClick(el.attachmentAdd, () => openAttachmentDialog("add"));
el.attachmentCancel.addEventListener("click", () => el.attachmentDialog.close());
el.attachmentDialog.addEventListener("cancel", event => { if (el.attachmentForm.inert) event.preventDefault(); });
el.attachmentDialog.addEventListener("close", () => {
el.attachmentForm.reset();
delete el.attachmentDialog.attachmentEdit;
});
el.attachmentForm.addEventListener("submit", async event => {
event.preventDefault();
const edit = el.attachmentDialog.attachmentEdit;
if (!edit || edit.openSeq !== state.openSeq || !el.attachmentForm.reportValidity()) return;
const files = Array.from(el.attachmentFile.files);
const names = edit.action === "rename" ? [el.attachmentName.value] : files.map(file => file.name);
if (await changeDocument("ofdgoChangeAttachment", null, edit.action, edit.id, names, files)) {
el.attachmentDialog.close();
setStatus({add:"附件已添加", rename:"附件已改名", replace:"附件已替换"}[edit.action]);
}
});
function openAttachmentDialog(action, attachment) {
if (!state.editing || !state.ready || state.exporting || document.body.hasAttribute("aria-busy")) return;
const rename = action === "rename";
el.attachmentForm.reset();
el.attachmentDialog.attachmentEdit = {action, id: attachment?.id || "", openSeq: state.openSeq};
el.attachmentTitle.textContent = {add:"添加附件", rename:"附件改名", replace:"替换附件"}[action];
el.attachmentSubmit.textContent = {add:"添加", rename:"确定", replace:"替换"}[action];
el.attachmentFileRow.hidden = rename;
el.attachmentFile.required = !rename;
el.attachmentFile.multiple = action === "add";
el.attachmentNameRow.hidden = !rename;
el.attachmentName.required = rename;
el.attachmentName.value = attachment?.name || "";
el.attachmentStatus.textContent = "";
el.attachmentDialog.showModal();
(rename ? el.attachmentName : el.attachmentFile).focus();
}
async function saveDocumentImage(resource) {
if (!resource || !state.doc || document.body.hasAttribute("aria-busy")) return;
const seq = state.openSeq;
@@ -7241,7 +7291,7 @@ function renderMeta(keepDetails = false) {
el.pageTotal.textContent = String(doc.pageCount || 0);
if (keepDetails && doc.detailsPending) return;
el.metaSignatures.textContent = doc.detailsPending ? "正在检查签名" : doc.detailsError ? "读取失败" : String(doc.signatureCount || 0);
renderMetaContent(el.attachmentList, [doc.attachments, doc.attachmentError], renderAttachments);
renderMetaContent(el.attachmentList, [doc.attachments, doc.attachmentError, state.editing], renderAttachments);
renderMetaContent(el.signatureList, [doc.signatures, doc.signatureError], renderSignatures);
renderMetaContent(el.docFontList, doc.fonts || [], renderDocumentFonts);
refreshEditorFonts();
@@ -7256,8 +7306,9 @@ function renderMetaContent(node, value, render) {
}
function renderAttachments() {
const attachments = state.doc?.attachments || [];
el.attachmentPanel.hidden = !attachments.length && !state.doc?.attachmentError;
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");
@@ -7267,17 +7318,25 @@ function renderAttachments() {
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}`);
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)].filter(Boolean).join(" · ");
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";
@@ -7285,7 +7344,30 @@ function renderAttachments() {
item.append(meta);
}
item.addEventListener("click", () => downloadAttachment(attachment));
el.attachmentList.append(item);
row.append(item);
if (state.editing) {
const actions = document.createElement("select");
actions.className = "small-button attachment-actions";
actions.title = "附件操作";
actions.setAttribute("aria-label", `${attachment.name} 操作`);
for (const [value, text] of [["", "操作"], ["rename", "改名"], ["replace", "替换"], ["delete", "删除"]]) {
const option = document.createElement("option");
option.value = value;
option.textContent = text;
actions.append(option);
}
actions.addEventListener("change", async () => {
const action = actions.value;
actions.value = "";
if ((canvasEditor.nudge || canvasEditor.nudgeCommit) && !await canvasEditor.commitNudge()) return;
if (!await canvasEditor.commitText() || !await canvasEditor.commitCrop()) return;
if (action === "delete") {
if (window.confirm(`删除附件“${attachment.name}”?`) && await changeDocument("ofdgoChangeAttachment", null, action, attachment.id, [], [])) setStatus("附件已删除");
} else if (action) openAttachmentDialog(action, attachment);
});
row.append(actions);
}
el.attachmentList.append(row);
}
}
@@ -8005,6 +8087,8 @@ function updateFitSpace() {
function updateControls() {
renderSecurity();
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);
updateDisplayControls();
updateEditorTools();
@@ -8217,6 +8301,8 @@ function setBusy(busy, text = "", percent = 0, status = "") {
el.pageForm.inert = busy;
el.paragraphForm.inert = busy;
el.infoForm.inert = busy;
el.attachmentForm.inert = busy;
el.attachmentPanel.inert = busy;
el.annotationForm.inert = busy;
el.importForm.inert = busy && !state.importing;
for (const input of [el.importFile, el.importRange, el.importPosition, el.importOutlines]) input.disabled = busy;
+7
View File
@@ -173,6 +173,13 @@ async function handleMessage({ id, name, args }) {
const password = converting ? args[2] : name === "ofdgoSetEncryption" ? args[0]?.password : name === "ofdgoOpen" ? args[3]?.password : name === "ofdgoLoadImport" ? args[1]?.password : null;
try {
signal?.throwIfAborted();
if (name === "ofdgoChangeAttachment" && (args[0] === "add" || args[0] === "replace")) {
const files = args[3], reader = new FileReaderSync();
args[3] = index => {
try { return { bytes: new Uint8Array(reader.readAsArrayBuffer(files[index])) }; }
catch (err) { return { error: err.message }; }
};
}
if (name === "ofdgoImportImages") {
const files = args[0], reader = new FileReaderSync();
args[0] = files.map(file => file.name);