Compare commits

..
2 Commits
Author SHA1 Message Date
xiaoqidun 3311f27786 docs(更新文档): 更新文档细节
build.yaml / build (push) Successful in 35s
2026-07-03 21:25:59 +08:00
xiaoqidun 94f0cf2362 feat(前端界面): 优化OFDGo WebUI文案 2026-07-03 21:25:05 +08:00
3 changed files with 44 additions and 25 deletions
+2 -2
View File
@@ -12,11 +12,11 @@ docker run -d -p 80:80 ccr.ccs.tencentyun.com/xiaoqidun/ofdgo:latest
# 构建指南 # 构建指南
```batch ```batch
:: 1. 编译WASM资源 :: 1. 编译OFDGo WASM
set GOOS=js set GOOS=js
set GOARCH=wasm set GOARCH=wasm
go build -o assets/webui/ofdgo.wasm -trimpath -ldflags "-s -w -buildid=" ./cmd/webui/wasm.go go build -o assets/webui/ofdgo.wasm -trimpath -ldflags "-s -w -buildid=" ./cmd/webui/wasm.go
:: 2. 构建OFDGo WebUI :: 2. 编译OFDGo WebUI
set GOOS=windows set GOOS=windows
set GOARCH=amd64 set GOARCH=amd64
go build -o ofdgo_webui.exe -trimpath -ldflags "-s -w -buildid=" ./cmd/webui/webui.go go build -o ofdgo_webui.exe -trimpath -ldflags "-s -w -buildid=" ./cmd/webui/webui.go
+2 -2
View File
@@ -10,7 +10,7 @@
<div class="app"> <div class="app">
<header class="toolbar app-bar"> <header class="toolbar app-bar">
<input id="ofdInput" class="file-input" type="file" accept=".ofd,application/ofd,application/zip"> <input id="ofdInput" class="file-input" type="file" accept=".ofd,application/ofd,application/zip">
<button id="ofdButton" class="button primary" type="button" data-short="打开">打开 OFD</button> <button id="ofdButton" class="button primary" type="button" data-short="打开">打开</button>
<input id="fontInput" class="file-input" type="file" accept=".ttf,.otf,.ttc,font/*" multiple> <input id="fontInput" class="file-input" type="file" accept=".ttf,.otf,.ttc,font/*" multiple>
<button id="togglePagesButton" class="icon-button" type="button" title="页面栏" aria-label="页面栏" aria-pressed="true">&#9776;</button> <button id="togglePagesButton" class="icon-button" type="button" title="页面栏" aria-label="页面栏" aria-pressed="true">&#9776;</button>
<button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">&#9432;</button> <button id="toggleMetaButton" class="icon-button" type="button" title="信息栏" aria-label="信息栏" aria-pressed="true">&#9432;</button>
@@ -98,7 +98,7 @@
<footer class="app-footer app-bar"> <footer class="app-footer app-bar">
<a class="footer-brand" href="https://github.com/xiaoqidun/ofdgo" target="_blank" rel="noreferrer">OFDGo</a> <a class="footer-brand" href="https://github.com/xiaoqidun/ofdgo" target="_blank" rel="noreferrer">OFDGo</a>
<div id="statusText" class="status-text">正在准备渲染引擎</div> <div id="statusText" class="status-text">正在准备引擎</div>
<nav class="footer-links" aria-label="相关链接"> <nav class="footer-links" aria-label="相关链接">
<a href="https://aite.me/" target="_blank" rel="noreferrer">官方网站</a> <a href="https://aite.me/" target="_blank" rel="noreferrer">官方网站</a>
</nav> </nav>
+40 -21
View File
@@ -21,10 +21,10 @@ const FIT_WIDTH_MARGIN = 72;
const FIT_HEIGHT_MARGIN = 86; const FIT_HEIGHT_MARGIN = 86;
const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)"); const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)");
const STATUS = { const STATUS = {
ready: "选择 OFD 文件开始阅读", ready: "选择 OFD 文件",
opening: "正在打开 OFD", opening: "正在打开 OFD",
engine: "正在准备渲染引擎", engine: "正在准备引擎",
recovering: "正在恢复渲染引擎", recovering: "正在恢复引擎",
fonts: "正在匹配字体", fonts: "正在匹配字体",
exporting: "正在导出 PDF", exporting: "正在导出 PDF",
}; };
@@ -135,10 +135,18 @@ COMPACT_LAYOUT.addEventListener("change", syncLayoutMode);
el.viewerPanel.addEventListener("scroll", () => { el.viewerPanel.addEventListener("scroll", () => {
schedulePageSync(); schedulePageSync();
}); });
el.viewerPanel.addEventListener("dblclick", openOFDFromViewer);
updateSidebarState(); updateSidebarState();
boot(); boot();
function openOFDFromViewer() {
if (state.doc || document.body.hasAttribute("aria-busy")) {
return;
}
el.ofdInput.click();
}
function toggleSidebar(side) { function toggleSidebar(side) {
if (side === "pages") { if (side === "pages") {
state.showPages = !state.showPages; state.showPages = !state.showPages;
@@ -168,10 +176,10 @@ function syncLayoutMode(event) {
} }
async function boot() { async function boot() {
setBusy(true, "准备渲染引擎", 8, STATUS.engine); setBusy(true, "正在准备引擎", 8, STATUS.engine);
try { try {
await ensureWASM(); await ensureWASM();
setProgress("准备就绪", 70); setProgress("引擎准备完成", 70);
setEmpty("选择 OFD 文件"); setEmpty("选择 OFD 文件");
updateFontSummary(); updateFontSummary();
renderFontList(); renderFontList();
@@ -210,18 +218,18 @@ async function loadWASM() {
clearWASMCallbacks(); clearWASMCallbacks();
const go = new Go(); const go = new Go();
if (!wasmModule) { if (!wasmModule) {
setProgress("下载渲染引擎", 16); setProgress("正在下载引擎", 16);
const response = await fetch("./ofdgo.wasm"); const response = await fetch("./ofdgo.wasm");
try { try {
setProgress("编译渲染引擎", 35); setProgress("正在编译引擎", 35);
wasmModule = await WebAssembly.compileStreaming(response.clone()); wasmModule = await WebAssembly.compileStreaming(response.clone());
} catch { } catch {
const bytes = await response.arrayBuffer(); const bytes = await response.arrayBuffer();
setProgress("编译渲染引擎", 45); setProgress("正在编译引擎", 45);
wasmModule = await WebAssembly.compile(bytes); wasmModule = await WebAssembly.compile(bytes);
} }
} }
setProgress("启动渲染引擎", 58); setProgress("正在启动引擎", 58);
const instance = await WebAssembly.instantiate(wasmModule, go.importObject); const instance = await WebAssembly.instantiate(wasmModule, go.importObject);
go.run(instance).then(() => { go.run(instance).then(() => {
markWASMExited(wasmSeq); markWASMExited(wasmSeq);
@@ -270,7 +278,7 @@ async function recoverWASM() {
state.wasmRecoveries += 1; state.wasmRecoveries += 1;
const pageIndex = state.pageIndex; const pageIndex = state.pageIndex;
const fitMode = state.fitMode || "width"; const fitMode = state.fitMode || "width";
setBusy(true, "恢复渲染引擎", 18, STATUS.recovering); setBusy(true, "正在恢复引擎", 18, STATUS.recovering);
try { try {
await ensureWASM(); await ensureWASM();
await openDocument({ await openDocument({
@@ -311,7 +319,7 @@ async function openSelectedOFD() {
return; return;
} }
state.wasmRecoveries = 0; state.wasmRecoveries = 0;
setBusy(true, "读取 OFD 文件", 10, STATUS.opening); setBusy(true, "正在读取 OFD", 10, STATUS.opening);
try { try {
state.fileName = file.name || "ofdgo.ofd"; state.fileName = file.name || "ofdgo.ofd";
state.ofdBytes = new Uint8Array(await file.arrayBuffer()); state.ofdBytes = new Uint8Array(await file.arrayBuffer());
@@ -340,7 +348,7 @@ async function loadLocalFonts() {
setStatus("当前浏览器不支持读取系统字体"); setStatus("当前浏览器不支持读取系统字体");
return; return;
} }
setBusy(true, state.doc ? "匹配系统字体" : "请求系统字体授权", 12, state.doc ? STATUS.fonts : "正在请求系统字体授权"); setBusy(true, state.doc ? "正在匹配字体" : "正在请求授权", 12, state.doc ? STATUS.fonts : "正在请求授权");
try { try {
await nextFrame(); await nextFrame();
const available = await queryLocalFonts(); const available = await queryLocalFonts();
@@ -395,7 +403,7 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) {
const emptyStatus = available.length === 0 ? "未读取到系统字体" : "未匹配到文档所需字体"; const emptyStatus = available.length === 0 ? "未读取到系统字体" : "未匹配到文档所需字体";
const fonts = []; const fonts = [];
for (let i = 0; i < selected.length; i += 1) { for (let i = 0; i < selected.length; i += 1) {
setProgress(`读取系统字体 ${i + 1}/${selected.length}`, 20 + Math.round(i / Math.max(1, selected.length) * 60)); setProgress(`正在读取字体 ${i + 1}/${selected.length}`, 20 + Math.round(i / Math.max(1, selected.length) * 60));
const item = selected[i]; const item = selected[i];
const blob = await item.blob(); const blob = await item.blob();
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
@@ -727,9 +735,9 @@ async function openDocument(options = {}) {
updateFontSummary(); updateFontSummary();
renderFontList(); renderFontList();
} }
setBusy(true, "打开 OFD", 20, STATUS.opening); setBusy(true, "正在打开 OFD", 20, STATUS.opening);
try { try {
setProgress("解析 OFD", 52); setProgress("正在解析 OFD", 52);
await nextFrame(); await nextFrame();
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
return; return;
@@ -745,7 +753,7 @@ async function openDocument(options = {}) {
state.scale = 1; state.scale = 1;
state.fitMode = options.fitMode || "width"; state.fitMode = options.fitMode || "width";
if (!options.skipAutoFonts && state.systemFontCatalog.length > 0) { if (!options.skipAutoFonts && state.systemFontCatalog.length > 0) {
setProgress("匹配系统字体", 62, STATUS.fonts); setProgress("正在匹配字体", 62, STATUS.fonts);
if (await loadDocumentLocalFonts(state.systemFontCatalog, openSeq)) { if (await loadDocumentLocalFonts(state.systemFontCatalog, openSeq)) {
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
return; return;
@@ -795,9 +803,9 @@ async function renderPage(index, options = {}) {
return; return;
} }
if (options.keepBusy) { if (options.keepBusy) {
setProgress("渲染页面", 76); setProgress("正在渲染页面", 76);
} else { } else {
setBusy(true, "渲染页面", 35, `正在渲染第 ${index + 1}`); setBusy(true, "正在渲染页面", 35, "正在渲染页面");
} }
try { try {
setCurrentPage(index); setCurrentPage(index);
@@ -832,17 +840,23 @@ async function exportPDF() {
return; return;
} }
const openSeq = state.openSeq; const openSeq = state.openSeq;
setBusy(true, "生成 PDF", 18, STATUS.exporting); el.exportButton.disabled = true;
setBusy(true, "正在准备 PDF", 18, STATUS.exporting);
try { try {
await nextFrame(); await waitForPaint();
if (openSeq !== state.openSeq) {
return;
}
setProgress("正在生成 PDF", 45);
await waitForPaint();
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
return; return;
} }
setProgress("导出 PDF", 45);
const result = callWASM("ofdgoExportPDF"); const result = callWASM("ofdgoExportPDF");
if (openSeq !== state.openSeq) { if (openSeq !== state.openSeq) {
return; return;
} }
setProgress("正在保存 PDF", 86);
const bytes = base64ToBytes(result.base64); const bytes = base64ToBytes(result.base64);
const blob = new Blob([bytes], { type: "application/pdf" }); const blob = new Blob([bytes], { type: "application/pdf" });
const link = document.createElement("a"); const link = document.createElement("a");
@@ -861,6 +875,7 @@ async function exportPDF() {
} finally { } finally {
if (openSeq === state.openSeq) { if (openSeq === state.openSeq) {
setBusy(false); setBusy(false);
updateControls();
} }
} }
} }
@@ -1610,3 +1625,7 @@ function formatBytes(size) {
function nextFrame() { function nextFrame() {
return new Promise((resolve) => requestAnimationFrame(resolve)); return new Promise((resolve) => requestAnimationFrame(resolve));
} }
function waitForPaint() {
return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
}