Z ZPI 文档 v0.3.8
介绍页

源码

packages/desktop/src/renderer/LegacyDocPreview.tsx

tsx133 行查看原文
// Adapted from ZCode previewPaneOfficeLegacyDocContent.tsx (Apache-2.0).

import {
  type DocModel,
  DocxEditorViewer,
  type DocxPageVirtualizationOptions,
  setWasmSource,
  useDocxEditor,
  useDocxModel,
} from "@extend-ai/react-docx";
import docxWasmUrl from "@extend-ai/react-docx/docx_wasm_bg.wasm?url";
import { useEffect, useLayoutEffect, useMemo } from "react";
import { logRendererError } from "./error-log.ts";
import { installDocumentLinkSafety } from "./office-preview-model.ts";
import { useDocxPreviewFit } from "./use-docx-preview-fit.ts";

setWasmSource(docxWasmUrl);

let nextDocxModelRenderKey = 0;
const docxModelRenderKeys = new WeakMap<DocModel, number>();

function getDocxModelRenderKey(model: DocModel): number {
  const existingKey = docxModelRenderKeys.get(model);
  if (existingKey !== undefined) {
    return existingKey;
  }
  nextDocxModelRenderKey += 1;
  docxModelRenderKeys.set(model, nextDocxModelRenderKey);
  return nextDocxModelRenderKey;
}

function ResponsiveDocxEditorViewer({
  model,
  onOpenBrowserUrl,
  sourcePath,
}: {
  model: DocModel;
  onOpenBrowserUrl?: (url: string) => void;
  sourcePath: string;
}) {
  // 轻量 ReactDocxViewer 不走编辑器的完整分页与排版链路,预览结果会和编辑画布不一致。
  // 使用同一个 editor controller 驱动文档画布,但固定为只读模式,避免暴露任何编辑能力。
  const editor = useDocxEditor({
    starterModel: model,
    initialFileName: sourcePath,
    initialDocumentTheme: "light",
  });
  const { viewportRef, contentRef, fit, frameStyle, contentStyle } = useDocxPreviewFit(sourcePath);
  // 外层使用 transform 缩放页面,但 react-docx 的虚拟列表不会自动识别 transform。
  // 必须同步传入相同 zoomScale,否则滚动坐标会按未缩放页高计算,导致闪白或末页无法挂载。
  const pageVirtualization = useMemo<DocxPageVirtualizationOptions>(
    () => ({ zoomScale: fit?.scale ?? 1 }),
    [fit?.scale],
  );

  useLayoutEffect(() => {
    const content = contentRef.current;
    return content ? installDocumentLinkSafety(content, onOpenBrowserUrl) : undefined;
  }, [contentRef, onOpenBrowserUrl, sourcePath]);

  return (
    <div ref={viewportRef} className="w-full min-w-0" data-docx-fit-viewport>
      <div className="relative mx-auto" data-docx-fit-frame style={frameStyle}>
        {/* react-docx 按纸张原始像素宽度渲染,窄 Preview Pane 会被 794px 页面撑破。
            只缩小不放大,并同步包装层宽高,避免单独 transform 后仍保留未缩放的横向滚动区。 */}
        <div
          ref={contentRef}
          className="theme-zai-light min-w-max text-foreground"
          data-docx-fit-content
          style={contentStyle}
        >
          <DocxEditorViewer
            className="min-w-max"
            editor={editor}
            mode="read-only"
            pageGapBackgroundColor="transparent"
            pageVirtualization={pageVirtualization}
          />
        </div>
      </div>
    </div>
  );
}

export default function LegacyDocPreview({
  buffer,
  errorMessage,
  onOpenBrowserUrl,
  sourcePath,
}: {
  buffer: ArrayBuffer;
  errorMessage: string;
  onOpenBrowserUrl?: (url: string) => void;
  sourcePath: string;
}) {
  const { model, isLoading, error } = useDocxModel(buffer);

  useEffect(() => {
    if (!error) {
      return;
    }
    logRendererError("doc-preview", error);
  }, [error, sourcePath]);

  if (isLoading) {
    return (
      <div aria-busy="true" className="h-full min-h-0 w-full bg-background" data-office-preview-pending />
    );
  }

  if (error || !model) {
    return (
      <div className="p-3 text-ui-base text-destructive" role="alert">
        {errorMessage}
      </div>
    );
  }

  return (
    <div
      className="h-full min-h-0 w-full min-w-0 overflow-auto bg-background p-4 max-sm:p-2"
      data-office-preview-kind="doc"
    >
      <ResponsiveDocxEditorViewer
        key={getDocxModelRenderKey(model)}
        model={model}
        onOpenBrowserUrl={onOpenBrowserUrl}
        sourcePath={sourcePath}
      />
    </div>
  );
}