feat: 发布 v0.4.0 桌面端
This commit is contained in:
@@ -2,6 +2,18 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
href="/apple-touch-icon.png"
|
||||
sizes="180x180"
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#1f2937" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' data: mdpdf:; img-src 'self' data: blob: mdpdf:; connect-src 'self' ws://localhost:5173; frame-src 'self' blob:; worker-src 'self' blob:;"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
name="description"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; script-src 'self'; style-src 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:; connect-src 'self';"
|
||||
content="default-src 'none'; script-src 'self'; style-src 'unsafe-inline'; font-src 'self' data: mdpdf:; img-src 'self' data: mdpdf:; connect-src 'self';"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<style>
|
||||
|
||||
+15
-3
@@ -37,9 +37,11 @@ import {
|
||||
downloadPdf,
|
||||
getCachedPdfExport,
|
||||
getOrRequestPdfExport,
|
||||
requestDesktopPdfExport,
|
||||
type CachedPdfExport,
|
||||
type PdfExportRequest
|
||||
} from "./pdf-export";
|
||||
import { APP_VERSION_LABEL } from "./app-version";
|
||||
import { getSyncedScrollTop } from "./scroll-sync";
|
||||
import {
|
||||
loadPreviewZoom,
|
||||
@@ -832,7 +834,10 @@ export function App() {
|
||||
try {
|
||||
const resolved = await getOrRequestPdfExport(
|
||||
pdfRequest,
|
||||
pdfCache
|
||||
pdfCache,
|
||||
previewPayload && window.mdToPdfDesktop
|
||||
? () => requestDesktopPdfExport(previewPayload)
|
||||
: undefined
|
||||
);
|
||||
if (pdfRequestKeyRef.current !== requestedKey) {
|
||||
setStatus("文档已更新,已丢弃过期 PDF");
|
||||
@@ -841,7 +846,11 @@ export function App() {
|
||||
setPdfCache(resolved);
|
||||
const exported = resolved.result;
|
||||
if (download) {
|
||||
downloadPdf(exported.blob, exported.fileName);
|
||||
const saved = await downloadPdf(exported.blob, exported.fileName);
|
||||
if (!saved) {
|
||||
setStatus("已取消 PDF 保存");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const pageDescription =
|
||||
exported.pageCount === undefined
|
||||
@@ -893,7 +902,10 @@ export function App() {
|
||||
<div className="topbar-primary">
|
||||
<div className="topbar-brand">
|
||||
<p className="eyebrow">内网文档工具</p>
|
||||
<h1>Markdown PDF 导出器</h1>
|
||||
<div className="topbar-brand-title">
|
||||
<h1>Markdown PDF 导出器</h1>
|
||||
<span className="app-version">{APP_VERSION_LABEL}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="document-summary">
|
||||
<span className="panel-kicker">源文件</span>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const APP_VERSION = __APP_VERSION__;
|
||||
export const APP_VERSION_LABEL = `v${APP_VERSION}`;
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { RenderedMarkdownDocument } from "@md-to-pdf/core";
|
||||
|
||||
export interface ThemeSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
bundled: boolean;
|
||||
source: "bundled" | "local";
|
||||
}
|
||||
|
||||
export interface MarkdownRenderInput {
|
||||
markdown: string;
|
||||
language: string;
|
||||
}
|
||||
|
||||
function throwIfAborted(signal?: AbortSignal) {
|
||||
signal?.throwIfAborted();
|
||||
}
|
||||
|
||||
async function readErrorMessage(
|
||||
response: Response,
|
||||
fallback: string
|
||||
) {
|
||||
try {
|
||||
const payload = (await response.json()) as { message?: unknown };
|
||||
return typeof payload.message === "string"
|
||||
? payload.message
|
||||
: fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export async function renderMarkdownDocument(
|
||||
input: MarkdownRenderInput,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
throwIfAborted(signal);
|
||||
if (window.mdToPdfDesktop) {
|
||||
const result = await window.mdToPdfDesktop.renderMarkdown(input);
|
||||
throwIfAborted(signal);
|
||||
return result;
|
||||
}
|
||||
|
||||
const response = await fetch("/api/render", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
...(signal ? { signal } : {})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(await readErrorMessage(response, "渲染失败"));
|
||||
}
|
||||
return response.json() as Promise<RenderedMarkdownDocument>;
|
||||
}
|
||||
|
||||
export async function listThemes(signal?: AbortSignal) {
|
||||
throwIfAborted(signal);
|
||||
if (window.mdToPdfDesktop) {
|
||||
const result = await window.mdToPdfDesktop.listThemes();
|
||||
throwIfAborted(signal);
|
||||
return result;
|
||||
}
|
||||
|
||||
const response = await fetch("/api/themes", {
|
||||
...(signal ? { signal } : {})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("无法加载主题清单");
|
||||
}
|
||||
return response.json() as Promise<{ themes: ThemeSummary[] }>;
|
||||
}
|
||||
|
||||
export async function getThemeCss(
|
||||
themeId: string,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
throwIfAborted(signal);
|
||||
if (window.mdToPdfDesktop) {
|
||||
const result = await window.mdToPdfDesktop.getThemeCss(themeId);
|
||||
throwIfAborted(signal);
|
||||
return result;
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`/api/themes/${encodeURIComponent(themeId)}/css`,
|
||||
{ ...(signal ? { signal } : {}) }
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("无法加载主题");
|
||||
}
|
||||
return response.text();
|
||||
}
|
||||
@@ -87,6 +87,30 @@ window.__mdToPdfRender = async (
|
||||
};
|
||||
document.documentElement.dataset.runtimeReady = "true";
|
||||
|
||||
const desktopPdfBridge = window.__mdToPdfDesktopPdf;
|
||||
if (desktopPdfBridge) {
|
||||
desktopPdfBridge.onRender(async (requestId, payload) => {
|
||||
const requestAnimationFrame = window.requestAnimationFrame;
|
||||
window.requestAnimationFrame = (callback) =>
|
||||
window.setTimeout(() => callback(performance.now()), 0);
|
||||
try {
|
||||
const result = await window.__mdToPdfRender?.(payload, "pdf");
|
||||
if (!result) {
|
||||
throw new Error("PDF 分页运行时不可用");
|
||||
}
|
||||
desktopPdfBridge.complete(requestId, result);
|
||||
} catch (reason: unknown) {
|
||||
desktopPdfBridge.fail(
|
||||
requestId,
|
||||
reason instanceof Error ? reason.message : "PDF 分页失败"
|
||||
);
|
||||
} finally {
|
||||
window.requestAnimationFrame = requestAnimationFrame;
|
||||
}
|
||||
});
|
||||
desktopPdfBridge.ready();
|
||||
}
|
||||
|
||||
window.addEventListener("message", (event: MessageEvent<unknown>) => {
|
||||
if (
|
||||
event.origin !== window.location.origin ||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ExportConfig } from "@md-to-pdf/core";
|
||||
import type {
|
||||
ExportConfig,
|
||||
PagedDocumentPayload
|
||||
} from "@md-to-pdf/core";
|
||||
|
||||
export interface PdfExportRequest {
|
||||
markdown: string;
|
||||
@@ -129,11 +132,38 @@ export async function requestPdfExport(
|
||||
};
|
||||
}
|
||||
|
||||
export function downloadPdf(blob: Blob, fileName: string) {
|
||||
export async function requestDesktopPdfExport(
|
||||
payload: PagedDocumentPayload
|
||||
): Promise<PdfExportResult> {
|
||||
const bridge = window.mdToPdfDesktop;
|
||||
if (!bridge) {
|
||||
throw new Error("桌面 PDF 引擎不可用");
|
||||
}
|
||||
|
||||
const result = await bridge.generatePdf(payload);
|
||||
const pdfBytes = Uint8Array.from(result.pdf);
|
||||
return {
|
||||
blob: new Blob([pdfBytes.buffer], { type: "application/pdf" }),
|
||||
fileName: payload.fileName.replace(/\.(md|markdown)$/i, "") + ".pdf",
|
||||
pageCount: result.pageCount,
|
||||
echartsErrorCount: result.echartsErrors.length,
|
||||
mermaidErrorCount: result.mermaidErrors.length
|
||||
};
|
||||
}
|
||||
|
||||
export async function downloadPdf(blob: Blob, fileName: string) {
|
||||
if (window.mdToPdfDesktop) {
|
||||
return window.mdToPdfDesktop.savePdf(
|
||||
fileName,
|
||||
new Uint8Array(await blob.arrayBuffer())
|
||||
);
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = fileName;
|
||||
anchor.click();
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,24 @@ button:disabled {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.topbar-brand-title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
padding: 2px 7px;
|
||||
border: 1px solid #c8d5cf;
|
||||
border-radius: 999px;
|
||||
background: #edf3f0;
|
||||
color: #527061;
|
||||
font-size: 0.66rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.035em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.document-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1029,6 +1047,12 @@ textarea:focus {
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.topbar-brand-title {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.settings-drawer {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { RenderedMarkdownDocument } from "@md-to-pdf/core";
|
||||
import { renderMarkdownDocument } from "./application-backend";
|
||||
|
||||
export interface MarkdownRenderCallbacks {
|
||||
onRenderStart?: () => void;
|
||||
@@ -23,27 +24,10 @@ export function useMarkdownRender(
|
||||
onRenderStart?.();
|
||||
setError("");
|
||||
|
||||
void fetch("/api/render", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ markdown, language }),
|
||||
signal: controller.signal
|
||||
})
|
||||
.then(async (response) => {
|
||||
const payload = (await response.json()) as
|
||||
| RenderedMarkdownDocument
|
||||
| { message?: string };
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
"message" in payload
|
||||
? payload.message ?? "渲染失败"
|
||||
: "渲染失败"
|
||||
);
|
||||
}
|
||||
return payload as RenderedMarkdownDocument;
|
||||
})
|
||||
void renderMarkdownDocument(
|
||||
{ markdown, language },
|
||||
controller.signal
|
||||
)
|
||||
.then((payload) => {
|
||||
setResult(payload);
|
||||
onStatusChange?.("预览已更新");
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
getThemeCss,
|
||||
listThemes,
|
||||
type ThemeSummary
|
||||
} from "./application-backend";
|
||||
|
||||
export interface ThemeSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
bundled: boolean;
|
||||
source: "bundled" | "local";
|
||||
}
|
||||
export type { ThemeSummary } from "./application-backend";
|
||||
|
||||
export function useThemeResources(
|
||||
themeId: string,
|
||||
@@ -21,15 +19,7 @@ export function useThemeResources(
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
void fetch("/api/themes", {
|
||||
signal: controller.signal
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("无法加载主题清单");
|
||||
}
|
||||
return response.json() as Promise<{ themes: ThemeSummary[] }>;
|
||||
})
|
||||
void listThemes(controller.signal)
|
||||
.then(({ themes: availableThemes }) => {
|
||||
setThemes(availableThemes);
|
||||
setCatalogError("");
|
||||
@@ -51,15 +41,7 @@ export function useThemeResources(
|
||||
setThemeCss("");
|
||||
setCssError("");
|
||||
|
||||
void fetch(`/api/themes/${encodeURIComponent(themeId)}/css`, {
|
||||
signal: controller.signal
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("无法加载主题");
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
void getThemeCss(themeId, controller.signal)
|
||||
.then(setThemeCss)
|
||||
.catch((reason: unknown) => {
|
||||
if (!controller.signal.aborted) {
|
||||
|
||||
Vendored
+57
@@ -1 +1,58 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import type {
|
||||
PagedDocumentPayload,
|
||||
PagedDocumentRenderResult,
|
||||
RenderedMarkdownDocument
|
||||
} from "@md-to-pdf/core";
|
||||
|
||||
declare global {
|
||||
const __APP_VERSION__: string;
|
||||
|
||||
interface DesktopPdfResult {
|
||||
pdf: Uint8Array;
|
||||
pageCount: number;
|
||||
echartsErrors: string[];
|
||||
mermaidErrors: string[];
|
||||
}
|
||||
|
||||
interface DesktopApplicationBridge {
|
||||
renderMarkdown(input: {
|
||||
markdown: string;
|
||||
language: string;
|
||||
}): Promise<RenderedMarkdownDocument>;
|
||||
listThemes(): Promise<{
|
||||
themes: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
bundled: boolean;
|
||||
source: "bundled" | "local";
|
||||
}>;
|
||||
}>;
|
||||
getThemeCss(themeId: string): Promise<string>;
|
||||
generatePdf(payload: PagedDocumentPayload): Promise<DesktopPdfResult>;
|
||||
savePdf(fileName: string, pdf: Uint8Array): Promise<boolean>;
|
||||
}
|
||||
|
||||
interface DesktopPdfRuntimeBridge {
|
||||
ready(): void;
|
||||
onRender(
|
||||
listener: (
|
||||
requestId: number,
|
||||
payload: PagedDocumentPayload
|
||||
) => Promise<void> | void
|
||||
): () => void;
|
||||
complete(
|
||||
requestId: number,
|
||||
result: PagedDocumentRenderResult
|
||||
): void;
|
||||
fail(requestId: number, message: string): void;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
mdToPdfDesktop?: DesktopApplicationBridge;
|
||||
__mdToPdfDesktopPdf?: DesktopPdfRuntimeBridge;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
APP_VERSION,
|
||||
APP_VERSION_LABEL
|
||||
} from "../src/app-version";
|
||||
|
||||
describe("应用版本", () => {
|
||||
it("从统一构建版本生成标题徽标", () => {
|
||||
expect(APP_VERSION).toBe("0.4.0");
|
||||
expect(APP_VERSION_LABEL).toBe("v0.4.0");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
afterEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from "vitest";
|
||||
import {
|
||||
getThemeCss,
|
||||
listThemes,
|
||||
renderMarkdownDocument
|
||||
} from "../src/application-backend";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("应用后端适配器", () => {
|
||||
it("在桌面环境通过窄 IPC 调用共享服务", async () => {
|
||||
const rendered = {
|
||||
rendererVersion: 1 as const,
|
||||
articleHtml: '<article id="write">测试</article>',
|
||||
bodyHtml: "测试",
|
||||
metadata: {
|
||||
title: "测试",
|
||||
author: "",
|
||||
subject: "",
|
||||
keywords: [],
|
||||
language: "zh-CN"
|
||||
},
|
||||
features: [],
|
||||
warnings: []
|
||||
};
|
||||
const renderMarkdown = vi.fn(async () => rendered);
|
||||
const listDesktopThemes = vi.fn(async () => ({
|
||||
themes: [
|
||||
{
|
||||
id: "test",
|
||||
name: "测试",
|
||||
version: "1",
|
||||
description: "",
|
||||
bundled: true,
|
||||
source: "bundled" as const
|
||||
}
|
||||
]
|
||||
}));
|
||||
const getDesktopThemeCss = vi.fn(async () => "#write {}");
|
||||
vi.stubGlobal("window", {
|
||||
mdToPdfDesktop: {
|
||||
renderMarkdown,
|
||||
listThemes: listDesktopThemes,
|
||||
getThemeCss: getDesktopThemeCss
|
||||
}
|
||||
});
|
||||
|
||||
await expect(
|
||||
renderMarkdownDocument({
|
||||
markdown: "# 测试",
|
||||
language: "zh-CN"
|
||||
})
|
||||
).resolves.toBe(rendered);
|
||||
await expect(listThemes()).resolves.toEqual({
|
||||
themes: [expect.objectContaining({ id: "test" })]
|
||||
});
|
||||
await expect(getThemeCss("test")).resolves.toBe("#write {}");
|
||||
expect(renderMarkdown).toHaveBeenCalledWith({
|
||||
markdown: "# 测试",
|
||||
language: "zh-CN"
|
||||
});
|
||||
expect(getDesktopThemeCss).toHaveBeenCalledWith("test");
|
||||
});
|
||||
|
||||
it("在浏览器环境保持现有 HTTP 协议", async () => {
|
||||
vi.stubGlobal("window", {});
|
||||
const fetcher = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(
|
||||
Response.json({
|
||||
rendererVersion: 1,
|
||||
articleHtml: '<article id="write">测试</article>',
|
||||
bodyHtml: "测试",
|
||||
metadata: {
|
||||
title: "测试",
|
||||
author: "",
|
||||
subject: "",
|
||||
keywords: [],
|
||||
language: "zh-CN"
|
||||
},
|
||||
features: [],
|
||||
warnings: []
|
||||
})
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
Response.json({ themes: [] })
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
new Response("#write {}", { status: 200 })
|
||||
);
|
||||
vi.stubGlobal("fetch", fetcher);
|
||||
|
||||
await renderMarkdownDocument({
|
||||
markdown: "# 测试",
|
||||
language: "zh-CN"
|
||||
});
|
||||
await listThemes();
|
||||
await getThemeCss("typora-like");
|
||||
|
||||
expect(fetcher).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"/api/render",
|
||||
expect.objectContaining({ method: "POST" })
|
||||
);
|
||||
expect(fetcher).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"/api/themes",
|
||||
{}
|
||||
);
|
||||
expect(fetcher).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
"/api/themes/typora-like/css",
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,30 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { defaultExportConfig } from "@md-to-pdf/core";
|
||||
import {
|
||||
afterEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from "vitest";
|
||||
import {
|
||||
createPagedDocumentPayload,
|
||||
defaultExportConfig
|
||||
} from "@md-to-pdf/core";
|
||||
import {
|
||||
createPdfExportCacheKey,
|
||||
downloadPdf,
|
||||
getCachedPdfExport,
|
||||
getOrRequestPdfExport,
|
||||
parseContentDispositionFileName,
|
||||
requestDesktopPdfExport,
|
||||
requestPdfExport,
|
||||
type CachedPdfExport,
|
||||
type PdfExportRequest
|
||||
} from "../src/pdf-export";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("PDF 导出客户端", () => {
|
||||
it("仅复用与当前文档和导出配置完全匹配的 PDF", () => {
|
||||
const request: PdfExportRequest = {
|
||||
@@ -178,4 +193,49 @@ describe("PDF 导出客户端", () => {
|
||||
)
|
||||
).rejects.toThrow("PDF 生成队列已满");
|
||||
});
|
||||
|
||||
it("桌面端使用共享分页载荷生成并保存 PDF", async () => {
|
||||
const payload = createPagedDocumentPayload({
|
||||
document: {
|
||||
rendererVersion: 1,
|
||||
articleHtml: '<article id="write">测试</article>',
|
||||
bodyHtml: "测试",
|
||||
metadata: {
|
||||
title: "测试",
|
||||
author: "",
|
||||
subject: "",
|
||||
keywords: [],
|
||||
language: "zh-CN"
|
||||
},
|
||||
features: [],
|
||||
warnings: []
|
||||
},
|
||||
fileName: "测试.md",
|
||||
themeCss: "",
|
||||
exportConfig: defaultExportConfig
|
||||
});
|
||||
const generatePdf = vi.fn(async () => ({
|
||||
pdf: new TextEncoder().encode("%PDF-desktop"),
|
||||
pageCount: 2,
|
||||
echartsErrors: [],
|
||||
mermaidErrors: ["图表 1"]
|
||||
}));
|
||||
const savePdf = vi.fn(async () => true);
|
||||
vi.stubGlobal("window", {
|
||||
mdToPdfDesktop: { generatePdf, savePdf }
|
||||
});
|
||||
|
||||
const generated = await requestDesktopPdfExport(payload);
|
||||
const saved = await downloadPdf(generated.blob, generated.fileName);
|
||||
|
||||
expect(generatePdf).toHaveBeenCalledWith(payload);
|
||||
expect(generated.fileName).toBe("测试.pdf");
|
||||
expect(generated.pageCount).toBe(2);
|
||||
expect(generated.mermaidErrorCount).toBe(1);
|
||||
expect(savePdf).toHaveBeenCalledWith(
|
||||
"测试.pdf",
|
||||
expect.any(Uint8Array)
|
||||
);
|
||||
expect(saved).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const rootPackage = JSON.parse(
|
||||
readFileSync(
|
||||
fileURLToPath(new URL("../../package.json", import.meta.url)),
|
||||
"utf8"
|
||||
)
|
||||
) as { version: string };
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
publicDir: fileURLToPath(
|
||||
new URL("../../logos/web", import.meta.url)
|
||||
),
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(rootPackage.version)
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
|
||||
Reference in New Issue
Block a user