feat(文档管理): 优化文档管理与信息编辑
build.yaml / build (push) Successful in 47s

This commit is contained in:
xiaoqidun committed 2026-10-03 15:33:14 +08:00
1 parent 0385e8c5fd
commit c084682351
8 files changed
+384 -64

No files matched your search

+16 -9
View File
@@ -332,6 +332,21 @@
<div class="form-actions"><div id="paragraphStatus" class="form-status" role="status"></div><button id="paragraphCancel" class="button" type="button">取消</button><button class="button" type="submit">确定</button></div>
</form>
</dialog>
<dialog id="documentPanel" class="form-dialog document-dialog" aria-labelledby="documentTitle">
<form id="documentForm">
<h2 id="documentTitle" class="form-title">文档管理</h2>
<div id="documentList" class="document-list" role="listbox" aria-label="文档列表"></div>
<div id="documentActions" class="document-actions" role="group" aria-label="文档操作">
<button id="documentAddButton" class="button" type="button" title="新增文档" aria-label="新增文档" aria-haspopup="dialog" aria-controls="createPanel">新增</button>
<button id="infoButton" class="button" type="button" title="修改文档" aria-label="修改文档信息" aria-haspopup="dialog" aria-controls="infoPanel">修改</button>
<button id="documentDeleteButton" class="button" type="button" title="删除文档" aria-label="删除文档">删除</button>
<button id="documentUpButton" class="button" type="button" title="前移文档" aria-label="前移文档">前移</button>
<button id="documentDownButton" class="button" type="button" title="后移文档" aria-label="后移文档">后移</button>
<button id="documentClose" class="button" type="button">关闭</button>
</div>
<div id="documentStatus" class="form-status" role="status"></div>
</form>
</dialog>
<dialog id="infoPanel" class="form-dialog" aria-label="文档信息">
<form id="infoForm">
<label class="form-row"><span>标题</span><input id="infoTitle" type="text" autofocus></label>
@@ -594,19 +609,11 @@
</section>
<div class="panel-row">
<div class="panel-title compact">文档</div>
<div id="documentActions" class="panel-actions" hidden>
<button id="documentAddButton" class="small-button" type="button" title="新建文档" aria-haspopup="dialog" aria-controls="createPanel">新建</button>
<button id="infoButton" class="small-button" type="button" title="修改文档" aria-label="修改文档信息" aria-haspopup="dialog" aria-controls="infoPanel" hidden>修改</button>
<button id="documentDeleteButton" class="small-button" type="button" title="删除文档">删除</button>
</div>
<button id="documentManageButton" class="small-button" type="button" title="文档管理" aria-label="文档管理" aria-haspopup="dialog" aria-controls="documentPanel" hidden>管理</button>
</div>
<div id="editNotice" class="font-permission-hint" role="status" hidden></div>
<div id="documentRow" class="document-row" hidden>
<select id="documentSelect" class="document-select" aria-label="选择文档" title="切换文档"></select>
<div id="documentOrder" class="panel-actions" hidden>
<button id="documentUpButton" class="small-button" type="button" title="前移文档" aria-label="前移文档">&#8593;</button>
<button id="documentDownButton" class="small-button" type="button" title="后移文档" aria-label="后移文档">&#8595;</button>
</div>
</div>
<dl class="meta-list">
<div>
+80 -10
View File
@@ -1429,6 +1429,7 @@ body[data-hide-meta] .meta-panel {
.viewer-panel,
.search-results,
.editor-toolbar,
.document-list,
.meta-panel {
scrollbar-width: thin;
scrollbar-color: var(--line-strong) transparent;
@@ -1523,16 +1524,6 @@ body[data-hide-meta] .meta-panel {
margin-bottom: 10px;
}
.document-row .panel-actions {
flex-wrap: nowrap;
}
.document-row .small-button {
width: 28px;
height: 28px;
padding: 0;
}
.document-select {
flex: 1;
width: 0;
@@ -1542,6 +1533,81 @@ body[data-hide-meta] .meta-panel {
font-size: 12px;
}
.document-dialog {
width: 440px;
}
.document-dialog .form-status:not(:empty) {
margin-top: 8px;
}
.document-actions {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 6px;
}
.document-actions > .button {
min-width: 0;
padding-inline: 6px;
}
.document-list {
max-height: 45dvh;
min-height: 80px;
margin-bottom: 12px;
border: 1px solid var(--line);
overflow: auto;
}
.document-entry {
display: grid;
grid-template-columns: minmax(0, 1fr) 4em;
align-items: center;
gap: 8px;
width: 100%;
min-height: 38px;
padding: 8px 10px;
border: 0;
border-bottom: 1px solid var(--line);
text-align: left;
}
.document-entry:last-child {
border-bottom: 0;
}
.document-entry:hover,
.document-entry[aria-selected="true"] {
background: var(--accent-soft);
}
.document-entry[aria-selected="true"] {
color: var(--accent-strong);
}
.document-entry:focus-visible {
outline-offset: -2px;
}
.document-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.document-pages {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
}
.document-pages {
text-align: right;
font-variant-numeric: tabular-nums;
}
.app-panel {
margin-bottom: 12px;
padding-bottom: 12px;
@@ -3127,6 +3193,10 @@ audio.page-media {
grid-template-columns: 1fr;
}
.document-actions {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.tool-group + .tool-group::before {
display: none;
}
+156 -19
View File
@@ -60,6 +60,9 @@ const state = {
imageImporting: false,
createInsert: false,
createPackage: false,
createDocumentAt: null,
documentManagement: null,
infoDocumentIndex: null,
pageSelection: new Set(),
pageSelectionAnchor: null,
pageMultiSelect: false,
@@ -235,8 +238,13 @@ const el = {
infoButton: document.querySelector("#infoButton"),
documentSelect: document.querySelector("#documentSelect"),
documentActions: document.querySelector("#documentActions"),
documentManageButton: document.querySelector("#documentManageButton"),
documentPanel: document.querySelector("#documentPanel"),
documentForm: document.querySelector("#documentForm"),
documentList: document.querySelector("#documentList"),
documentStatus: document.querySelector("#documentStatus"),
documentClose: document.querySelector("#documentClose"),
documentRow: document.querySelector("#documentRow"),
documentOrder: document.querySelector("#documentOrder"),
documentAddButton: document.querySelector("#documentAddButton"),
documentDeleteButton: document.querySelector("#documentDeleteButton"),
documentUpButton: document.querySelector("#documentUpButton"),
@@ -679,30 +687,54 @@ editorClick(el.paragraphButton, () => {
});
el.paragraphCancel.addEventListener("click", () => el.paragraphPanel.close());
editorClick(el.documentAddButton, () => openCreatePanel(false, true));
editorClick(el.documentManageButton, openDocumentManagement);
el.documentClose.addEventListener("click", () => el.documentPanel.close());
el.documentPanel.addEventListener("cancel", event => { if (el.documentForm.inert) event.preventDefault(); });
el.documentPanel.addEventListener("close", () => {
if (!el.documentPanel.open) {
state.documentManagement = null;
el.documentList.replaceChildren();
el.documentStatus.textContent = "";
}
});
el.documentList.addEventListener("keydown", event => {
if (!state.documentManagement || !["ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return;
event.preventDefault();
const count = state.doc.documents.length, index = state.documentManagement.index;
selectManagedDocument(event.key === "Home" ? 0 : event.key === "End" ? count - 1
: Math.max(0, Math.min(count - 1, index + (event.key === "ArrowUp" ? -1 : 1))), true);
});
editorClick(el.documentDeleteButton, () => {
if (window.confirm("删除当前文档?")) return changeDocument("ofdgoChangeDocument", null, "delete", state.doc.documentIndex || 0);
const index = managedDocumentIndex(), entry = state.doc.documents[index];
if (window.confirm(`删除文档“${entry.title || "未命名"}”?`)) return changeDocument("ofdgoChangeDocument", null, "delete", index);
});
editorClick(el.documentUpButton, () => {
const index = state.doc.documentIndex || 0;
const index = managedDocumentIndex();
return changeDocument("ofdgoChangeDocument", null, "move", index, index - 1);
});
editorClick(el.documentDownButton, () => {
const index = state.doc.documentIndex || 0;
const index = managedDocumentIndex();
return changeDocument("ofdgoChangeDocument", null, "move", index, index + 1);
});
editorClick(el.infoButton, () => {
for (const key of ["Title", "Author", "Subject"]) el[`info${key}`].value = state.doc[key.toLowerCase()] || "";
state.infoDocumentIndex = managedDocumentIndex();
const info = state.documentManagement?.entries?.[state.infoDocumentIndex] || state.doc;
for (const key of ["Title", "Author", "Subject"]) el[`info${key}`].value = info[key.toLowerCase()] || "";
el.infoCustomRows.replaceChildren();
for (const [index, field] of (state.doc.customData || []).entries()) addCustomDataRow(field, index);
for (const [index, field] of (info.customData || []).entries()) addCustomDataRow(field, index);
el.infoStatus.textContent = "";
el.infoPanel.showModal();
});
el.infoCancel.addEventListener("click", () => el.infoPanel.close());
el.infoPanel.addEventListener("cancel", event => { if (el.infoForm.inert) event.preventDefault(); });
el.infoPanel.addEventListener("close", () => { if (!el.infoPanel.open) state.infoDocumentIndex = null; });
el.infoCustomAdd.addEventListener("click", () => addCustomDataRow());
el.infoForm.addEventListener("submit", async event => {
event.preventDefault();
const fields = [...el.infoCustomRows.children].map(row => ({Name: row.children[0].value, Value: row.children[1].value, Source: row.sourceIndex}));
if (await changeDocument("ofdgoUpdateInfo", null, el.infoTitle.value, el.infoAuthor.value, el.infoSubject.value, JSON.stringify(fields))) el.infoPanel.close();
const args = [el.infoTitle.value, el.infoAuthor.value, el.infoSubject.value, JSON.stringify(fields)];
if (state.infoDocumentIndex !== null) args.push(state.infoDocumentIndex);
if (await changeDocument("ofdgoUpdateInfo", null, ...args)) el.infoPanel.close();
});
function addCustomDataRow(field = {Name: "", Value: ""}, sourceIndex) {
@@ -1529,7 +1561,7 @@ function handleKeyDown(event) {
}
function formDialogOpen() {
return Boolean(sealPreview) || el.signPanel.open || el.verifyPanel.open || el.credentialsPanel.open || el.encryptionPanel.open || batchElements.Panel.open || el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open
return Boolean(sealPreview) || el.signPanel.open || el.verifyPanel.open || el.credentialsPanel.open || el.encryptionPanel.open || batchElements.Panel.open || el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open || el.documentPanel.open
|| el.batchPagesPanel.open || el.objectStylePanel.open || el.sourceTextPanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open || el.annotationCreate.open || el.objectPicker.open;
}
@@ -1956,6 +1988,93 @@ function setEditorInfo(doc) {
setDirty(state.packageDirty || state.encryptionDirty || doc.revision !== state.savedRevision);
}
function managedDocumentIndex() {
return state.documentManagement?.index ?? state.doc?.documentIndex ?? 0;
}
function documentEntryKey(entry) {
return JSON.stringify([entry?.root, entry?.id]);
}
async function openDocumentManagement() {
if (!state.editing || !state.ready || state.exporting || document.body.hasAttribute("aria-busy")) return;
const index = state.doc.documentIndex || 0;
state.documentManagement = { index, key: documentEntryKey(state.doc.documents[index]), entries: null, loading: false, failed: false };
renderDocumentManagement();
el.documentPanel.showModal();
await refreshDocumentManagement(true);
}
async function refreshDocumentManagement(focus = false) {
const manager = state.documentManagement, openSeq = state.openSeq;
if (!manager || !el.documentPanel.open) return;
manager.loading = true;
manager.failed = false;
el.documentStatus.textContent = "正在读取文档";
updateDocumentControls();
try {
const entries = await callWASM("ofdgoPackageDocuments");
if (state.documentManagement !== manager || !el.documentPanel.open) return;
if (openSeq !== state.openSeq) return await refreshDocumentManagement(focus);
const index = entries.findIndex(entry => documentEntryKey(entry) === manager.key);
manager.index = index < 0 ? Math.min(manager.index, entries.length - 1) : index;
manager.entries = entries;
manager.key = documentEntryKey(entries[manager.index]);
el.documentStatus.textContent = "";
renderDocumentManagement();
if (focus) el.documentList.children[manager.index]?.focus();
} catch (error) {
if (state.documentManagement === manager && openSeq === state.openSeq) {
manager.failed = true;
el.documentStatus.textContent = error.message;
}
} finally {
if (state.documentManagement === manager) {
manager.loading = false;
updateDocumentControls();
}
}
}
function renderDocumentManagement() {
const manager = state.documentManagement;
if (!manager) return;
const entries = manager.entries || state.doc.documents;
el.documentList.replaceChildren(...entries.map(entry => {
const button = document.createElement("button");
button.type = "button";
button.className = "document-entry";
button.setAttribute("role", "option");
button.setAttribute("aria-selected", String(entry.index === manager.index));
button.tabIndex = entry.index === manager.index ? 0 : -1;
const name = document.createElement("span");
name.className = "document-name";
name.textContent = `${entry.index + 1} · ${entry.title || "未命名"}`;
button.title = name.textContent;
const pages = document.createElement("span");
pages.className = "document-pages";
const count = entry.pageCount ?? (entry.index === state.doc.documentIndex ? state.doc.pageCount : null);
pages.textContent = count === null ? "-" : `${count}页`;
button.append(name, pages);
button.addEventListener("click", () => selectManagedDocument(entry.index));
return button;
}));
updateDocumentControls();
}
function selectManagedDocument(index, focus = false) {
const manager = state.documentManagement;
if (!manager || manager.loading || document.body.hasAttribute("aria-busy") || index < 0 || index >= state.doc.documents.length) return;
manager.index = index;
manager.key = documentEntryKey(state.doc.documents[index]);
for (const [i, button] of [...el.documentList.children].entries()) {
button.setAttribute("aria-selected", String(i === index));
button.tabIndex = i === index ? 0 : -1;
}
updateDocumentControls();
if (focus) el.documentList.children[index]?.focus();
}
async function switchDocument(index) {
if (!state.doc || document.body.hasAttribute("aria-busy") || state.exporting || formDialogOpen()) return;
const previous = state.doc.documentIndex || 0;
@@ -2087,9 +2206,10 @@ function openCreatePanel(insert = false, packageDocument = false) {
el.createForm.reset();
state.createInsert = insert === true;
state.createPackage = packageDocument === true;
state.createDocumentAt = state.createPackage ? managedDocumentIndex() + 1 : null;
el.createMode.value = state.createInsert ? "images" : "blank";
el.createSubmit.textContent = state.createInsert ? "插入" : "新建";
el.createPanel.setAttribute("aria-label", state.createInsert ? "插入图片" : "新建文档");
el.createSubmit.textContent = state.createInsert ? "插入" : state.createPackage ? "新增" : "新建";
el.createPanel.setAttribute("aria-label", state.createInsert ? "插入图片" : state.createPackage ? "新增文档" : "新建文档");
el.createCancel.disabled = false;
updateCreateOptions();
el.createStatus.textContent = "";
@@ -2130,7 +2250,7 @@ async function createDocument(event) {
event.preventDefault();
if (state.createPackage) {
if (await changeDocument("ofdgoChangeDocument", null, "add", el.createName.value.trim() || "未命名",
Number(el.createWidth.value), Number(el.createHeight.value), (state.doc.documentIndex || 0) + 1)) el.createPanel.close();
Number(el.createWidth.value), Number(el.createHeight.value), state.createDocumentAt)) el.createPanel.close();
return;
}
if (state.createInsert) {
@@ -2884,6 +3004,7 @@ async function changeDocument(name, item, ...args) {
const restoring = name === "ofdgoUndo" || name === "ofdgoRedo";
const { scrollLeft, scrollTop } = el.viewerPanel;
const importing = name === "ofdgoImportPages" || name === "ofdgoImportImages";
let refreshManagement = false;
state.imageImporting = name === "ofdgoImportImages";
if (importing) state.importing = true;
setBusy(true, importing ? "正在导入页面" : "", null);
@@ -2916,12 +3037,16 @@ async function changeDocument(name, item, ...args) {
if (doc.revision === state.editorInfo?.revision) {
return true;
}
refreshManagement = el.documentPanel.open && (name === "ofdgoChangeDocument" || name === "ofdgoUpdateInfo" || restoring);
const previousDocument = state.doc.documents?.[state.doc.documentIndex || 0];
const nextDocument = doc.documents?.[doc.documentIndex || 0];
const documentChanged = previousDocument?.root !== nextDocument?.root || previousDocument?.id !== nextDocument?.id;
const packageChanged = documentChanged || name === "ofdgoChangeDocument" || restoring
&& JSON.stringify(doc.documents?.map(entry => [entry.root, entry.id])) !== JSON.stringify(state.doc.documents?.map(entry => [entry.root, entry.id]));
if (packageChanged) {
if (state.documentManagement && name === "ofdgoChangeDocument" && args[0] === "add") {
state.documentManagement.key = documentEntryKey(nextDocument);
}
resetCompositeScope();
state.objectClipboard = state.styleClipboard = null;
state.pageSelection.clear();
@@ -3010,6 +3135,7 @@ async function changeDocument(name, item, ...args) {
return true;
}
} catch (err) {
refreshManagement = false;
if (openSeq === state.openSeq) {
if (el.annotationCreate.open) {
el.annotationCreateStatus.textContent = err.message;
@@ -3037,6 +3163,8 @@ async function changeDocument(name, item, ...args) {
el.createStatus.textContent = err.message;
} else if (el.importPanel.open) {
el.importStatus.textContent = err.message;
} else if (el.documentPanel.open) {
el.documentStatus.textContent = err.message;
} else {
showError(err, false);
}
@@ -3052,6 +3180,7 @@ async function changeDocument(name, item, ...args) {
}
if (openSeq === state.openSeq) {
setBusy(false);
if (refreshManagement) await refreshDocumentManagement();
if (toolbarFocus?.isConnected && !toolbarFocus.disabled) toolbarFocus.focus({ preventScroll: true });
}
}
@@ -3722,6 +3851,9 @@ async function openOFD(file, remote = null) {
el.pagePanel.close();
el.paragraphPanel.close();
el.infoPanel.close();
el.documentPanel.close();
state.documentManagement = null;
state.infoDocumentIndex = null;
el.annotationNote.close();
el.annotationCreate.close();
el.objectPicker.close();
@@ -8305,15 +8437,20 @@ function updateFitSpace() {
}
function updateDocumentControls() {
const count = state.doc?.documents?.length || 0, index = state.doc?.documentIndex || 0;
const disabled = !state.doc || !state.ready || state.exporting || document.body.hasAttribute("aria-busy");
el.documentActions.hidden = el.documentOrder.hidden = !state.editing;
const count = state.doc?.documents?.length || 0, index = managedDocumentIndex(), manager = state.documentManagement;
const busy = document.body.hasAttribute("aria-busy");
const disabled = !state.doc || !state.ready || state.exporting || busy;
el.documentManageButton.hidden = !state.editing;
el.documentManageButton.disabled = disabled;
el.documentRow.hidden = !count || !state.editing && count < 2;
el.documentSelect.disabled = disabled;
el.documentAddButton.disabled = el.infoButton.disabled = disabled || !state.editing;
el.documentDeleteButton.disabled = disabled || !state.editing || count <= 1;
el.documentUpButton.disabled = disabled || !state.editing || index === 0;
el.documentDownButton.disabled = disabled || !state.editing || index >= count - 1;
const actionsDisabled = disabled || !state.editing || Boolean(manager?.loading || manager?.failed);
el.documentList.inert = busy || Boolean(manager?.loading);
el.documentAddButton.disabled = actionsDisabled;
el.infoButton.disabled = actionsDisabled || Boolean(manager && !manager.entries);
el.documentDeleteButton.disabled = actionsDisabled || count <= 1;
el.documentUpButton.disabled = actionsDisabled || index === 0;
el.documentDownButton.disabled = actionsDisabled || index >= count - 1;
}
function updateControls() {
@@ -8355,7 +8492,6 @@ function updateEditorTools() {
el.editButton.title = state.editing ? "阅读" : "编辑";
el.editButton.setAttribute("aria-label", el.editButton.title);
el.editNotice.textContent = state.editorInfo?.editWarnings?.join(";") || "";
el.infoButton.hidden = !state.editing;
el.editNotice.hidden = !state.editing || !el.editNotice.textContent;
el.editorTools.hidden = !state.editing;
const pagesDisabled = !state.editing || !state.ready || state.exporting;
@@ -8535,6 +8671,7 @@ function setBusy(busy, text = "", percent = 0, status = "") {
el.pageForm.inert = busy;
el.paragraphForm.inert = busy;
el.infoForm.inert = busy;
el.documentForm.inert = busy;
el.attachmentForm.inert = busy;
el.attachmentPanel.inert = busy;
el.annotationForm.inert = busy;
+44 -4
View File
@@ -186,6 +186,7 @@ func RunWASM() {
registerCallback("ofdgoOpen", openDocument)
registerCallback("ofdgoSelectDocument", selectDocument)
registerCallback("ofdgoChangeDocument", changePackageDocument)
registerCallback("ofdgoPackageDocuments", packageDocuments)
registerAsyncCallback("ofdgoConvertPDF", convertPDFDocument)
registerAsyncCallback("ofdgoConvertFile", convertFile)
registerAsyncCallback("ofdgoPackFiles", packFiles)
@@ -3285,11 +3286,21 @@ func createDocument(args []js.Value) (any, error) {
}
// updateInfo 修改文档信息和自定义字段,保留其他元数据及创建程序标识
// 入参: args 标题、作者、主题、可选自定义字段JSON
// 入参: args 标题、作者、主题、可选自定义字段JSON及文档索引
// 返回: any 编辑状态, error 错误信息
func updateInfo(args []js.Value) (any, error) {
return changeObjects(func() error {
info := currentEditor.Info
if len(args) < 3 || len(args) > 5 {
return fmt.Errorf("invalid document info arguments")
}
index := currentEditor.DocumentIndex()
if len(args) > 4 {
index = args[4].Int()
}
info, err := currentEditor.DocumentInfo(index)
if err != nil {
return err
}
info.Title, info.Author, info.Subject = args[0].String(), args[1].String(), args[2].String()
if len(args) > 3 {
var submitted []struct {
@@ -3317,11 +3328,40 @@ func updateInfo(args []js.Value) (any, error) {
info.CustomDatas = &ofdgo.CustomDatas{CustomData: fields}
}
}
currentEditor.SetInfo(info)
return nil
return currentEditor.SetDocumentInfo(index, info)
})
}
// packageDocuments 按需读取管理列表的元数据和页数,不切换当前文档或解析页面内容
// 入参: args 保留参数
// 返回: any 文档列表, error 编辑状态错误
func packageDocuments(args []js.Value) (any, error) {
if currentSession == nil || currentEditor == nil {
return nil, fmt.Errorf("no document is being edited")
}
entries := make([]DocumentEntry, currentEditor.DocumentCount())
for i := range entries {
info, err := currentEditor.DocumentInfo(i)
if err != nil {
return nil, err
}
entry := &entries[i]
body := currentSession.Reader.OFD.DocBody[i]
entry.Index, entry.Root, entry.ID = i, body.DocRoot, info.DocID
entry.Title, entry.Author, entry.Subject = info.Title, info.Author, info.Subject
if info.CustomDatas != nil {
entry.CustomData = info.CustomDatas.CustomData
}
if i == currentEditor.DocumentIndex() {
count := currentEditor.PageCount()
entry.PageCount = &count
} else if count, err := currentSession.Reader.DocumentPageCount(i); err == nil {
entry.PageCount = &count
}
}
return entries, nil
}
// changeAttachment 编辑附件,逐个读取文件并复用库层事务和历史
// 入参: args 操作、附件标识、名称列表、文件读取回调
// 返回: any 编辑状态, error 错误信息
+5 -1
View File
@@ -98,12 +98,16 @@ type DocumentInfo struct {
DetailsPending bool `json:"detailsPending,omitempty"`
}
// DocumentEntry 包内文档入口,不预先解析其他文档的页面和资源
// DocumentEntry 包内文档入口,管理列表按需补充元数据和页数
type DocumentEntry struct {
Index int `json:"index"`
Title string `json:"title"`
Root string `json:"root"`
ID string `json:"id"`
Author string `json:"author,omitempty"`
Subject string `json:"subject,omitempty"`
CustomData []ofdgo.CustomData `json:"customData,omitempty"`
PageCount *int `json:"pageCount,omitempty"`
}
// DocumentDetails 不含页面数组的文档补充信息
+49 -18
View File
@@ -23,6 +23,7 @@ import (
"io"
"maps"
"path"
"reflect"
"slices"
"strings"
)
@@ -45,6 +46,50 @@ func (e *Editor) DocumentIndex() int {
return e.source.reader.DocumentIndex()
}
// DocumentInfo 获取指定文档信息的独立副本,不切换当前文档
// 入参: index 文档索引,从0开始
// 返回: DocInfo 文档信息, error 索引错误
func (e *Editor) DocumentInfo(index int) (DocInfo, error) {
if index < 0 || index >= e.DocumentCount() {
return DocInfo{}, fmt.Errorf("document index %d out of range", index)
}
if index == e.DocumentIndex() {
return cloneEditorData(e.Info), nil
}
info := cloneEditorData(e.source.reader.OFD.DocBody[index].DocInfo)
if info.CustomDatas != nil {
for i := range info.CustomDatas.CustomData {
info.CustomDatas.CustomData[i].sourceIndex = i + 1
}
}
return info, nil
}
// SetDocumentInfo 修改指定文档信息,保留扩展内容,不切换当前文档,可通过Undo撤销
// 入参: index 文档索引,从0开始, info 文档信息
// 返回: error 修改错误
func (e *Editor) SetDocumentInfo(index int, info DocInfo) error {
before, err := e.DocumentInfo(index)
if err != nil {
return err
}
if reflect.DeepEqual(before, info) {
return nil
}
if index == e.DocumentIndex() {
e.SetInfo(info)
return nil
}
return e.editDocuments(func(reader *Reader, data []byte, _ *editorXML) (int, error) {
updated, err := editorUpdateInfoXML(data, NewEditor().Info.DocID, index, before, info)
if err != nil {
return 0, err
}
reader.files[reader.fileNamesFold["ofd.xml"]] = updated
return reader.DocumentIndex(), nil
})
}
// AddDocument 在包内插入含一页的空白文档并选中,不修改输入包
// 当前文档的修改保留,新文档使用独立目录和文档标识;可通过Undo撤销
// 入参: title 文档标题, width 页面宽度, height 页面高度, at 插入位置,从0开始,可等于文档数量
@@ -116,26 +161,12 @@ func (e *Editor) AddDocument(title string, width, height float64, at int) (int,
// 入参: index 文档索引,从0开始, title 标题,空值移除标题
// 返回: error 修改错误
func (e *Editor) RenameDocument(index int, title string) error {
if index < 0 || index >= e.DocumentCount() {
return fmt.Errorf("document index %d out of range", index)
}
if index == e.DocumentIndex() {
info := e.Info
info.Title = title
e.SetInfo(info)
return nil
}
if e.source.reader.OFD.DocBody[index].DocInfo.Title == title {
return nil
}
return e.editDocuments(func(reader *Reader, data []byte, _ *editorXML) (int, error) {
data, info, err := editorDocInfoXML(data, NewEditor().Info.DocID, index)
info, err := e.DocumentInfo(index)
if err != nil {
return 0, err
return err
}
reader.files[reader.fileNamesFold["ofd.xml"]] = editorXMLSetText(data, info, [][2]string{{"Title", title}})
return reader.DocumentIndex(), nil
})
info.Title = title
return e.SetDocumentInfo(index, info)
}
// MoveDocument 调整包内文档顺序,当前文档随条目移动,不重写内容,可通过Undo撤销
+10 -3
View File
@@ -29,16 +29,23 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
if err != nil {
return nil, err
}
data, info, err := editorDocInfoXML(data, e.source.fallbackDocID, e.source.reader.documentIndex)
return editorUpdateInfoXML(data, e.source.fallbackDocID, e.source.reader.documentIndex, e.source.info, e.Info)
}
// editorUpdateInfoXML 更新指定文档的描述和自定义字段,保留未修改内容
// 入参: data 根索引XML, docID 缺少描述时使用的标识, index 文档索引, before 原信息, after 新信息
// 返回: []byte 修改后的XML, error 解析错误
func editorUpdateInfoXML(data []byte, docID string, index int, before, after DocInfo) ([]byte, error) {
data, info, err := editorDocInfoXML(data, docID, index)
if err != nil {
return nil, err
}
before, after := e.source.info, e.Info
var values [][2]string
for _, field := range [][3]string{
{"DocID", before.DocID, after.DocID}, {"Title", before.Title, after.Title}, {"Author", before.Author, after.Author},
{"Subject", before.Subject, after.Subject}, {"Abstract", before.Abstract, after.Abstract},
{"CreationDate", before.CreationDate, after.CreationDate}, {"ModDate", before.ModDate, after.ModDate},
{"Creator", before.Creator, after.Creator}, {"CreatorVersion", before.CreatorVersion, after.CreatorVersion},
} {
if field[1] != field[2] {
values = append(values, [2]string{field[0], field[2]})
@@ -50,7 +57,7 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
if err != nil {
return nil, err
}
return editorCustomDatas(data, root.childAt("DocBody", e.source.reader.documentIndex).child("DocInfo"), after.CustomDatas)
return editorCustomDatas(data, root.childAt("DocBody", index).child("DocInfo"), after.CustomDatas)
}
return data, nil
}
+24
View File
@@ -83,6 +83,30 @@ func (r *Reader) DocumentIndex() int {
return r.documentIndex
}
// DocumentPageCount 获取指定文档页数,不切换当前文档或加载资源、注释和签名
// 入参: index 文档索引,从0开始
// 返回: int 页数, error 错误信息
func (r *Reader) DocumentPageCount(index int) (int, error) {
if index < 0 || index >= r.DocumentCount() {
return 0, fmt.Errorf("document index %d out of range", index)
}
if index == r.documentIndex && r.doc != nil {
return len(r.doc.Pages.Page), nil
}
data, err := r.readFile(r.OFD.DocBody[index].DocRoot)
if err != nil {
return 0, err
}
var doc struct {
XMLName xml.Name `xml:"Document"`
Pages Pages `xml:"Pages"`
}
if err := xml.Unmarshal(data, &doc); err != nil {
return 0, fmt.Errorf("failed to unmarshal document.xml: %w", err)
}
return len(doc.Pages.Page), nil
}
// Document 创建指定文档的独立阅读器,不改变当前文档或混用资源缓存
// 返回的阅读器不关闭输入文件,使用期间不得关闭持有输入文件的原阅读器
// 入参: index 文档索引,从0开始