React 组件

inklayer-react 提供 PdfAnnotatorPdfViewer 两个顶层组件,基于 React 18+ 构建,使用 Zustand 管理状态,支持 ThemeProvider 和国际化。

安装

npm install inklayer-react

基础导入

import { PdfAnnotator, PdfViewer } from 'inklayer-react'
import 'inklayer-react/style'

// 类型
import type { PdfAnnotatorProps, PdfViewerProps, User, IAnnotationStore } from 'inklayer-react'

PdfAnnotator

完整的 PDF 批注组件,内置工具栏、批注编辑、侧边栏、保存/加载/导出功能。

Props

Prop类型默认值说明
urlstring | URLPDF 文件的 URL 地址。与 data 互斥
datastring | number[] | ArrayBuffer | Uint8Array | Uint16Array | Uint32ArrayPDF 的二进制数据。与 url 互斥
appearance'auto' | 'dark' | 'light''auto'UI 外观模式。auto 跟随系统主题
theme见下方主题色表格'violet'UI 主题色
titleReact.ReactNode'PDF ANNOTATOR'批注器标题
locale'zh-CN' | 'en-US''zh-CN'界面语言
initialScale'auto' | 'page-actual' | 'page-fit' | 'page-width' | number'auto'初始缩放比例。'page-actual' 实际尺寸,'page-fit' 适应页宽,'page-width' 页宽模式
layoutStyleReact.CSSPropertiesundefined容器样式。必须显式传入,否则组件高度为 0。详见下方说明
user{ id: string, name: string }{ id: 'null', name: 'unknown' }当前用户信息,用于批注归属
enableNativeAnnotationsbooleanfalse是否启用 PDF.js 原生批注渲染
enableRangeboolean | 'auto''auto'PDF 范围加载模式。'auto' 自动判断
defaultOptionsDeepPartial<PdfAnnotatorOptions>默认批注配置选项(颜色、签名、印章)
initialAnnotationsAnnotation[][]初始批注数据(用于回显已有批注)
defaultShowAnnotationsSidebarbooleanfalse批注侧边栏默认是否展开
actionsReactNode | ((props: ActionsProps) => ReactNode)自定义操作区域,渲染在工具栏右侧。支持函数形式接收操作上下文

主题色选项

颜色值说明
gray灰色
gold金色
bronze青铜色
brown棕色
yellow黄色
amber琥珀色
orange橙色
tomato番茄色
red红色
ruby红宝石色
crimson深红色
pink粉色
plum梅子色
purple紫色
violet紫罗兰色(默认)
iris鸢尾花色
indigo靛蓝色
blue蓝色
cyan青色
teal蓝绿色
jade翡翠色
green绿色
grass草绿色
lime青柠色
mint薄荷色
sky天空色

事件回调

事件签名触发时机
onSave(annotations: Annotation[]) => void用户点击保存按钮后触发
onLoad() => voidPDF 加载完成后触发
onAnnotationAdded(annotation: Annotation) => void新批注创建后触发
onAnnotationDeleted(id: string) => void批注删除后触发
onAnnotationSelected(annotation: Annotation | null, isClick: boolean) => void批注被选中/取消选中时触发
onAnnotationUpdated(annotation: Annotation) => void批注更新(移动/编辑)后触发

完整示例:带持久化的批注器

import { PdfAnnotator } from 'inklayer-react'
import 'inklayer-react/style'
import type { Annotation } from 'inklayer-react'
import { useState, useCallback } from 'react'

export default function AnnotatorPage() {
  const [annotations, setAnnotations] = useState<Annotation[]>([])

  const handleSave = useCallback((annotations: Annotation[]) => {
    setAnnotations(annotations)
    // 将批注数据发送到后端
    fetch('/api/annotations', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(annotations),
    })
  }, [])

  return (
    <PdfAnnotator
      url="/document.pdf"
      locale="zh-CN"
      layoutStyle={{ height: '96vh' }}
      enableRange="auto"
      defaultShowAnnotationsSidebar={true}
      user={{ id: 'editor-1', name: '张三' }}
      initialAnnotations={annotations}
      defaultOptions={{
        colors: ['#FF0000', '#00FF00', '#0000FF'],
        signature: {
          defaultSignature: [],
          defaultFont: [{ label: '楷体', value: 'STKaiti', external: false }]
        },
        stamp: { defaultStamp: [] }
      }}
      onSave={handleSave}
      onLoad={() => console.log('PDF 加载完成')}
      onAnnotationAdded={(a) => console.log('新增批注:', a.id)}
      onAnnotationDeleted={(id) => console.log('删除批注:', id)}
      actions={(props) => (
        <>
          <button onClick={() => props.save()}>保存</button>
          <button onClick={() => console.log(props.getAnnotations())}>获取批注</button>
          <button onClick={() => props.exportToExcel('export')}>导出 Excel</button>
          <button onClick={() => props.exportToPdf('export')}>导出 PDF</button>
        </>
      )}
    />
  )
}

⚠️ 关于 layoutStyle

PdfAnnotatorPdfViewer 内部采用绝对定位布局,组件尺寸完全由 layoutStyle 控制,不会自适应父容器高度layoutStyle 是标准的 React.CSSProperties 对象,但所有数值必须以字符串形式传入(带 CSS 单位):

// ✅ 正确
<PdfAnnotator layoutStyle={{ width: '100%', height: '600px' }} ... />
<PdfAnnotator layoutStyle={{ width: '100vw', height: '100vh' }} ... />

// ❌ 错误(数值不带单位,组件无法正确计算尺寸)
<PdfAnnotator layoutStyle={{ width: 800, height: 600 }} ... />

常用布局方案:

// 方案 A:全屏占满
<PdfAnnotator layoutStyle={{ width: '100vw', height: '100vh' }} ... />

// 方案 B:嵌入有固定高度的容器
<div style={{ height: '600px' }}>
  <PdfAnnotator layoutStyle={{ width: '100%', height: '100%' }} ... />
</div>

// 方案 C:减去顶部导航栏高度
<PdfAnnotator layoutStyle={{ width: '100%', height: 'calc(100vh - 64px)' }} ... />

如果页面空白,第一个排查步骤:打开 React DevTools,检查 PdfAnnotator 的 DOM 元素是否有高度。

关于 onSave

onSave 在用户点击工具栏右侧的「保存」按钮时触发。回调收到的 annotations 是完整的 Annotation[] 格式,直接保存此数组即可,无需额外转换:

const handleSave = (annotations: Annotation[]) => {
  // annotations 已是完整数据,可直接发送
  fetch('/api/annotations', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(annotations),
  })
}

后端存储建议:如果需要在后端结构化存储批注数据,由后端自行决定存储格式。前端只需通过 onSave 获取完整的 Annotation[] 并发送即可。

PdfViewer

轻量级 PDF 查看器,不包含批注编辑功能。适合只需浏览 PDF 的场景。

Props(继承 PdfAnnotator 基础属性,新增以下)

Prop类型默认值说明
showTextLayerbooleantrue是否显示文本选区层
showAnnotationsbooleanfalse是否显示已有批注
defaultActiveSidebarKeystring | nullnull默认展开的侧边栏面板 key
toolbarReactNode | (ctx) => ReactNode自定义工具栏(支持 render props 获取上下文)
sidebarSidebarPanel[]自定义侧边栏面板

查看器完整示例

import { PdfViewer } from 'inklayer-react'
import 'inklayer-react/style'

export default function ViewerPage() {
  return (
    <PdfViewer
      url="/document.pdf"
      showTextLayer={true}
      showAnnotations={true}
      layoutStyle={{ height: '96vh' }}
      enableRange="auto"
      defaultActiveSidebarKey={null}
      actions={(context) => (
        <>
          <button onClick={() => context.print()}>打印</button>
          <button onClick={() => context.download('file')}>下载</button>
          <button onClick={context.toggleSidebar}>切换侧边栏</button>
        </>
      )}
      toolbar={(context) => (
        <div>
          <button onClick={() => context.pdfViewer?.scrollPageIntoView({ pageNumber: 1 })}>
            跳到第1页
          </button>
        </div>
      )}
      sidebar={[{
        key: 'custom-sidebar',
        title: '自定义面板',
        icon: <span>🔧</span>,
        render: (context) => (
          <div>
            <button onClick={context.toggleSidebar}>关闭</button>
          </div>
        )
      }]}
      onDocumentLoaded={(pdfViewer) => console.log('PDF 加载完成:', pdfViewer)}
      onEventBusReady={(eventBus) => {
        eventBus?.on('pagerendered', (e) => console.log('页面渲染:', e.pageNumber))
      }}
    />
  )
}

SidebarPanel 类型

sidebar prop 接受 SidebarPanel[],定义自定义侧边栏面板:

interface SidebarPanel {
  key: string                    // 面板唯一 key
  title: React.ReactNode        // 面板标题
  icon: React.ReactNode         // 面板图标
  render: (context: PdfViewerContextValue) => React.ReactNode  // 面板内容渲染函数
}

PdfViewerContextValue 类型

actionstoolbarsidebar 的函数形式接收 PdfViewerContextValue 对象:

interface PdfViewerContextValue {
  pdfViewer: PDFViewer | null    // PDFViewer 实例
  currentPage: number           // 当前页码(0-based)
  totalPages: number            // 总页数
  scale: number                 // 当前缩放比例
  print: () => void           // 打印
  download: (filename?: string) => void  // 下载
  toggleSidebar: () => void   // 切换侧边栏
  scrollPageIntoView: (opts: { pageNumber: number }) => void  // 滚动到指定页
}