更新记录

0.1.0(2026-06-01) 下载此版本

  • 初始 uni_modules 发布:SignPad、PdfSignViewer、ContractSign
  • 内置 sdk/core、sdk/platform

1.0(2026-05-29) 下载此版本

@smartwork/contract-sign-ui

uni-app 签署 UI 组件(P1–P3 逐步迁入)。

当前状态

组件 状态
utils/signLandscapeTouch.js ✅ 已迁移
SignPad ✅ P1 已完成(renderjs 画板)
PdfSignViewer ✅ P2 已完成(pdf.js 预览 + 锚点)
ContractSign ✅ P3 已完成(PdfSignViewer + SignPad + 提交)

安装(团队内)

将本目录复制到目标项目的 uni_modules/contract-sign-ui,或在 pages 中相对路径引用 packages/contract-sign-ui

组件 API

PdfSignViewer 快速示例

<PdfSignViewer
  :pdf-url="contract.filePath"
  :sign-position="contract.signPosition"
  :signed-map="signedMap"
  view-only
  @load="({ pageCount, anchors }) => {}"
  @anchor-click="({ anchorId, anchor }) => {}"
/>
<SignPad
  :visible.sync="visible"
  title="请签名"
  :landscape="true"
  pen-color="#000"
  :pen-width="2"
  @export="({ base64, pngBytes, hasStroke }) => { /* ... */ }"
/>
// ref 调用
this.$refs.signPad.open()
this.$refs.signPad.clear()
const { base64, bytes } = await this.$refs.signPad.exportPng()
this.$refs.signPad.close()

renderjs 注意

  • pdf.js worker 版本须与 pdfjs-dist 一致(当前 3.11.174
  • 画板继续用 renderjs + 指令,勿改回 Vue 层 canvas

平台兼容性

uni-app(4.0)

Vue2 Vue2插件版本 Vue3 Vue3插件版本 Chrome Safari app-vue app-vue插件版本 app-nvue app-nvue插件版本 Android Android插件版本 iOS 鸿蒙
0.1.0 0.1.0 0.1.0 0.1.0 6.0 0.1.0 - -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - - -

合同签署组件库 — 开发文档与使用手册

面向接入方:说明 packages/contract-sign-* 能做什么、怎么在项目里调用、如何单独使用 PDF 预览 / 画板 / 完整签署,以及 Core / Platform 层 JS API。
架构与抽取背景见 contract-sign-component-library.md


目录

  1. 功能概览
  2. 包结构与依赖
  3. 接入项目(5 分钟)
  4. signPosition 数据约定
  5. UI 组件 API
  6. Core / Platform JS API
  7. 使用场景手册
  8. 业务页薄封装模式
  9. Demo 与本地调试
  10. H5 / App 差异与配置
  11. 常见问题

1. 功能概览

本组件库从 smartworkapp 合同签署能力抽离,解决 uni-app 端 PDF 合同预览、多锚点手写签字、合成 PDF、上传 等通用问题;业务 API(拉合同、落库、跳转成功页)由业务项目注入

能力矩阵

能力 组件 / 模块 典型场景
PDF 多页预览 + 锚点框 <PdfSignViewer /> 查看已签合同、审批前预览
独立手写画板(导出 PNG base64 / 二进制) <SignPad /> 入库单签字、回执签名(无 PDF)
完整合同签署页 <ContractSign /> 网签协议、扫码签署
锚点坐标解析 contract-sign-core 与 Web 营收系统共用 JSON
PDF 合成签名图 contract-sign-core 服务端或客户端合成
PDF 下载 / 上传编排 contract-sign-platform uni-app 跨端适配

架构关系

flowchart TB
  subgraph business [业务项目]
    PAGE[业务页面 / 薄封装页]
    API[业务 API:拉合同、落库]
  end

  subgraph ui [contract-sign-ui]
    CS[ContractSign]
    PV[PdfSignViewer]
    SP[SignPad]
    RJS[pdf-render.js / sign-pad-render.js]
  end

  subgraph platform [contract-sign-platform]
    DL[pdfDownload]
    SF[submitFlow]
  end

  subgraph core [contract-sign-core]
    COORD[coordinate]
    MERGE[pdfMerge]
  end

  PAGE --> API
  PAGE --> CS
  CS --> PV
  CS --> SP
  CS --> RJS
  CS --> SF
  CS --> MERGE
  PV --> RJS
  PV --> DL
  SP --> MERGE
  MERGE --> COORD
  SF --> DL

设计原则

  • renderjs 画板:手写签字在 renderjs + HTML5 canvas 中完成,避免 Vue 层 canvas 与 PDF 层冲突(App / H5 一致)。
  • 底栏在 renderjs:签署模式下「签署 / 提交签署」按钮在 renderjs 内渲染,防止 PDF 层盖住 Vue 底栏。
  • 业务解耦:上传地址、落库接口通过 uploadFn / submitFn 注入,组件库不写死业务 API。

2. 包结构与依赖

packages/
  contract-sign-core/       # 纯逻辑:坐标、signPosition、pdf-lib 合成
  contract-sign-platform/   # uni-app:下载 PDF、提交流程编排
  contract-sign-ui/         # Vue 组件:ContractSign、PdfSignViewer、SignPad
  contract-sign-demo/       # 三个示例页(可复制)

npm 依赖(宿主项目需已安装)

用途
pdf-lib 合成签名到 PDF(core)
pdfjs-dist@^3.11.174 renderjs 渲染 PDF(ui)

静态资源

App 端 pdf.js Worker 使用:

static/pdf/pdf.worker.min.js

版本须与 package.jsonpdfjs-dist 一致(当前 3.11.174)。

引用方式

方式 A:相对路径(当前 smartworkapp 用法)

import ContractSign from '@/packages/contract-sign-ui/components/ContractSign/ContractSign.vue'
import { parseSignPositionPayload, mergeSignaturesIntoPdf } from '@/packages/contract-sign-core/src/index.js'
import { downloadPdfAsArrayBuffer, runContractSignSubmit } from '@/packages/contract-sign-platform/src/index.js'

方式 B:拷贝到其他项目

将整个 packages/contract-sign-* 目录复制到目标仓库,按相对路径引用;或发布为 uni_modules/contract-sign-ui(团队内推荐)。

方式 C:兼容旧路径(仅 smartworkapp 内)

utils/contractSignatureCoordinate.js 等已 re-export 到 packages,旧 import 仍可用,新代码建议直接用 packages。


3. 接入项目(5 分钟)

步骤 1:注册 Demo 页(可选)

pages.json 增加(本仓库已注册):

{
  "path": "packages/contract-sign-demo/pages/demo/sign-pad-only",
  "style": { "navigationBarTitleText": "SignPad Demo" }
}

步骤 2:最小签署页

<template>
  <ContractSign
    v-if="ready"
    :pdf-url="pdfUrl"
    :sign-position="signPosition"
    @complete="onComplete"
    @fail="onFail"
  />
</template>

<script>
import ContractSign from '@/packages/contract-sign-ui/components/ContractSign/ContractSign.vue'

export default {
  components: { ContractSign },
  data() {
    return {
      ready: false,
      pdfUrl: '',
      signPosition: null
    }
  },
  onLoad() {
    // 业务:请求合同详情后赋值
    this.loadContract().then((detail) => {
      this.pdfUrl = detail.filePath
      this.signPosition = detail.signPosition
      this.ready = true
    })
  },
  methods: {
    loadContract() {
      // 替换为你的 API
      return Promise.resolve({
        filePath: 'https://example.com/contract.pdf',
        signPosition: { pages: [...], anchors: [...] }
      })
    },
    onComplete({ fileUrl, raw }) {
      console.log('签署完成', fileUrl, raw)
    },
    onFail({ stage, error }) {
      console.error(stage, error)
    }
  }
}
</script>

步骤 3:H5 开发环境配置 PDF 代理(仅 H5)

若 PDF 在 OSS 且存在跨域,在 vue.config.js 配置 devServer 代理(本仓库已有 /pdf-proxy)。
组件内默认将 zennerfile.maobaoinfo.cn 转为 /pdf-proxy 路径;其他域名请传 h5-pdf-proxy(见 10. H5 / App 差异)。


4. signPosition 数据约定

Web 营收系统 使用同一套 JSON,由后端合同配置下发。组件库统一通过 parseSignPositionPayload 解析。

标准结构

{
  "pages": [
    { "pageNumber": 1, "widthPt": 595.28, "heightPt": 841.89 }
  ],
  "anchors": [
    {
      "anchorId": "a1",
      "label": "甲方签字",
      "pageNumber": 1,
      "boxNormalized": { "x": 0.1, "y": 0.8, "w": 0.2, "h": 0.08 }
    }
  ]
}
字段 说明
pages[].widthPt/heightPt 与 pdf.js getViewport({ scale: 1 }) 一致
anchors[].boxNormalized 相对单页左上角,0~1;兼容 Web 字段 nx/ny/nw/nh
boxPdfBottomLeftOriginPt 可选;后端已算好时优先用于 pdf-lib 落图

校验

import { validateSignPosition } from '@/packages/contract-sign-core/src/index.js'

const result = validateSignPosition(signPositionRaw)
if (!result.ok) {
  console.error(result.error)
} else {
  console.log(result.data.anchors)
}

只读预览(无锚点)

已签合同仅预览 PDF 时,signPosition 可传空字符串或 { anchors: [] },只传 pdfUrl 即可。


5. UI 组件 API

5.1 <SignPad /> — 独立手写画板

路径packages/contract-sign-ui/components/SignPad/SignPad.vue
uni_modulesuni_modules/contract-sign-ui/components/SignPad/SignPad.vue

不依赖 PDF、不依赖合同接口。适用于:入库确认签字、审批单签名等 只要 PNG 图片 的场景。

两条独立导出链路

base64 链路 PNG 二进制链路
适用场景 页面 <image> 预览、上传 base64 接口 写 PDF、pdf-lib embedPng、文件上传
ref 方法 exportPng() exportPngBytes()
事件 @export @export-bytes
返回值 / 载荷 { base64, hasStroke } Uint8Array / { bytes, hasStroke }
base64 说明 纯字符串, data:image/png;base64, 前缀 不含 base64

两条链路互不包装exportPng() 不返回 bytes;exportPngBytes() 不返回 base64。

Props

属性 类型 默认 说明
visible boolean false 是否显示弹层;支持 .sync / v-model:visible
title string '请签名' 弹层标题
landscape boolean true true:竖屏持机 + CSS 旋转横条画板
penColor string '#000' 笔触颜色
penWidth number 2 线宽
background string '#fff' 画布背景色(仅视觉,导出 PNG 为透明底笔画)
primaryColor string '#0081ff' 按钮主题色
closeOnExport boolean true 导出成功后是否自动关闭弹层
confirmExport 'export' \| 'export-bytes' 'export-bytes' 画板内点「确认」时走哪条导出链路

Events

事件 载荷 说明
open 弹层打开
close 弹层关闭
clear 清空画布(含点击「重签」)
change { hasStroke } 是否有笔画
export { base64, hasStroke } base64 链路:确认(confirmExport="export")或 exportPng()
export-bytes { bytes, hasStroke } 二进制链路:确认(默认)或 exportPngBytes()bytesUint8Array

Methods(ref)

方法 说明
open() 打开画板
close() 关闭
clear() 清空
exportPng() Promise<{ base64, hasStroke }>,并触发 @export
exportPngBytes() Promise<Uint8Array>,并触发 @export-bytes

示例 A:base64(预览 / base64 上传)

用户点「确认」或业务侧编程调用,走 base64 链路:

<template>
  <view>
    <button @click="showPad = true">客户签字</button>
    <SignPad
      :visible.sync="showPad"
      title="请客户在此签名"
      confirm-export="export"
      @export="onSignExport"
    />
    <image v-if="previewUrl" :src="previewUrl" mode="aspectFit" />
  </view>
</template>

<script>
import SignPad from '@/uni_modules/contract-sign-ui/components/SignPad/SignPad.vue'

export default {
  components: { SignPad },
  data() {
    return { showPad: false, previewUrl: '' }
  },
  methods: {
    onSignExport({ base64, hasStroke }) {
      if (!hasStroke || !base64) return
      this.previewUrl = 'data:image/png;base64,' + base64
      this.uploadBase64(base64)
    },
    uploadBase64(base64) {
      // uni.request / 自有 API
    }
  }
}
</script>

编程式导出 base64:

this.showPad = true
// 用户画完后(画板须保持打开):
try {
  const { base64, hasStroke } = await this.$refs.signPad.exportPng()
  if (!hasStroke || !base64) return
  const previewUrl = 'data:image/png;base64,' + base64
} catch (e) {
  // SignPad is not open / empty signature
}

示例 B:PNG 二进制(写 PDF / 文件上传)

用户点「确认」或业务侧编程调用,走二进制链路(ContractSign 内部同款):

<template>
  <view>
    <button @click="showPad = true">签字</button>
    <SignPad
      :visible.sync="showPad"
      title="请签名"
      confirm-export="export-bytes"
      @export-bytes="onSignBytes"
    />
  </view>
</template>

<script>
import SignPad from '@/uni_modules/contract-sign-ui/components/SignPad/SignPad.vue'

export default {
  components: { SignPad },
  data() {
    return { showPad: false }
  },
  methods: {
    onSignBytes({ bytes, hasStroke }) {
      if (!hasStroke || !bytes) return
      // bytes 为 Uint8Array
      this.uploadPngBytes(bytes)
    },
    uploadPngBytes(bytes) {
      // 直接上传 / mergeSignaturesIntoPdf / pdf-lib embedPng
    }
  }
}
</script>

编程式导出二进制:

try {
  const bytes = await this.$refs.signPad.exportPngBytes()
  // bytes: Uint8Array,无 base64
  await this.uploadPngBytes(bytes)
} catch (e) {
  // 画板未打开 / 未签名
}

合并进 PDF:

import { mergeSignaturesIntoPdf } from '@/uni_modules/contract-sign-ui/sdk/core/pdfMerge.js'

// signedMap: { [anchorId]: Uint8Array }
signedMap[anchorId] = bytes
const mergedPdf = await mergeSignaturesIntoPdf(pdfArrayBuffer, signPosition, signedMap)

画板内「确认」走哪条链路

confirm-export 控制,与 ref 方法无关:

<!-- 点确认 → @export(base64) -->
<SignPad confirm-export="export" @export="onBase64" />

<!-- 点确认 → @export-bytes(默认,bytes) -->
<SignPad confirm-export="export-bytes" @export-bytes="onBytes" />

怎么选

需求 推荐用法
<image> 预览 @exportexportPng(),自行拼 data:image/png;base64, 前缀
上传 PNG / 写 PDF @export-bytesexportPngBytes(),直接用 bytes
确认后自动关画板 close-on-export 默认 true
导出后继续编辑 :close-on-export="false"
监听是否有笔画 @change="({ hasStroke }) => {}"

注意

  1. ref 调用 exportPng() / exportPngBytes() 时,画板须已打开且已有签名,否则 Promise reject。
  2. 画板全屏遮罩会挡住页面内按钮;可让用户点画板「确认」,或将操作按钮浮在遮罩之上(z-index > 100000)。
  3. 空画布导出会 toast「请先手写签名」,并 reject(empty signature)。

本仓库测试页

pages/contractSignTest/index.vue(画板 Tab)同时演示 @export@export-bytes 及 ref 调用。


5.2 <PdfSignViewer /> — PDF 预览 + 锚点

路径packages/contract-sign-ui/components/PdfSignViewer/PdfSignViewer.vue

渲染多页 PDF,叠加签署锚点框;不含画板、不含底栏

Props

属性 类型 默认 说明
pdfUrl string '' PDF 地址(与 pdfBytes 二选一)
pdfBytes ArrayBuffer \| Uint8Array null 已有 PDF 二进制,避免二次下载
signPosition string \| object null 锚点配置
signedMap object {} { anchorId: Uint8Array \| base64 \| dataUrl } 已签预览
activeAnchorId string '' 高亮某锚点
viewOnly boolean true true 时锚点不可点击
previewTopPx number 0 预览区距顶部
previewBottomPx number 8 预览区距底部
height string '100vh' 容器最小高度
h5PdfProxy Function null H5 自定义 PDF URL 代理

Events

事件 载荷
load { pageCount, anchors }
anchor-click { anchorId, anchor }viewOnly=false 时)
fail { stage: 'download' \| 'render', error }

Methods(ref)

方法 说明
reload() 重新加载 PDF

示例:查看已签合同

<template>
  <PdfSignViewer
    :pdf-url="signedPdfUrl"
    :sign-position="signPosition"
    view-only
    height="100vh"
    @load="onLoad"
    @fail="onFail"
  />
</template>

<script>
import PdfSignViewer from '@/packages/contract-sign-ui/components/PdfSignViewer/PdfSignViewer.vue'

export default {
  components: { PdfSignViewer },
  data() {
    return {
      signedPdfUrl: 'https://oss.example.com/signed.pdf',
      signPosition: '' // 无锚点时可传空
    }
  },
  methods: {
    onLoad({ pageCount }) {
      console.log('共', pageCount, '页')
    },
    onFail({ stage, error }) {
      uni.showToast({ title: error.message, icon: 'none' })
    }
  }
}
</script>

示例:预览带已签图片的锚点

<PdfSignViewer
  :pdf-url="pdfUrl"
  :sign-position="signPosition"
  :signed-map="signedMap"
  view-only
/>
// signedMap 值可以是:
// 1. Uint8Array / ArrayBuffer(PNG 二进制)
// 2. base64 字符串(无 data: 前缀)
// 3. data:image/png;base64,... 完整 dataUrl
signedMap: {
  a1: pngUint8Array
}

5.3 <ContractSign /> — 完整合同签署

路径packages/contract-sign-ui/components/ContractSign/ContractSign.vue

组合 PDF 预览 + 锚点 + renderjs 底栏 + SignPad + 合成上传
viewOnly=true 时内部直接使用 <PdfSignViewer />

Props

属性 类型 必填 默认 说明
pdfUrl string pdfBytes 二选一 合同 PDF URL
pdfBytes ArrayBuffer \| Uint8Array 二选一 已有 PDF
signPosition string \| object 签署时必填 锚点配置
viewOnly boolean false 只预览,无底栏无画板
mode 'app' \| 'public' 'app' 影响顶栏:public 为扫码样式
title string 自动 导航标题
hideTopNav boolean false 隐藏返回钮(扫码页)
fileName string contract_signed_{ts}.pdf 合成/上传文件名
uploadFn Function 内置默认 见下方签名
submitFn Function 上传后业务落库
landscapeSign boolean true 画板横屏布局
theme object { primaryColor: '#0081ff' } 主题
h5PdfProxy Function H5 PDF 代理
autoShowUploadUrl boolean true 提交成功后是否弹上传链接窗

uploadFn 签名

(bytes: ArrayBuffer | Uint8Array, options: { fileName: string }) 
  => Promise<{ fileUrl?: string, raw?: object }>
  • 未传时:H5 走 uploadMergedPdfBytes;App 走 renderjs 内 FormData 上传(与原先合同页一致)。
  • 自定义:对接自有文件服务或网关。

submitFn 签名

(ctx: { fileUrl?: string, raw?: object, bytes: Uint8Array, fileName: string }) 
  => Promise<any>

用于上传成功后的 业务落库(如 save-sign-contract)。不传则只上传并 emit('complete')

Events

事件 载荷 说明
load { pageCount, anchors } PDF 渲染完成
anchor-select { anchorId, anchor } 选中锚点
sign-stroke { anchorId } 某锚点完成手写(未提交)
sign-change { signedMap, allSigned } 签名图变更
merge-success { bytes, fileName } pdf-lib 合成成功
upload-success { fileUrl, raw } 上传成功
submit-success { fileUrl, raw, submitResponse } submitFn 成功
complete 汇总对象 推荐业务只监听此事件
fail { stage, error } stage: download | render | sign | merge | upload | submit
back 用户点返回

Slots

插槽 说明
header 替换顶栏(默认蓝底导航 + 返回)

Methods(ref)

方法 说明
openSignPad(anchorId?) 打开画板
clearCurrentSign() 清除当前锚点签名
getSignedMap() 返回已签 map
mergePdf() 手动触发合成+提交流程(同底栏「提交签署」)
submit() mergePdf()

示例:完整签署 + 自定义落库

<template>
  <ContractSign
    :pdf-url="pdfUrl"
    :sign-position="signPosition"
    :upload-fn="myUpload"
    :submit-fn="mySubmit"
    :auto-show-upload-url="false"
    @complete="onComplete"
    @fail="onFail"
  />
</template>

<script>
import ContractSign from '@/packages/contract-sign-ui/components/ContractSign/ContractSign.vue'
import { uploadMergedPdfBytes } from '@/utils/contractPdfUpload.js'

export default {
  components: { ContractSign },
  data() {
    return {
      pdfUrl: '',
      signPosition: null,
      openAccountId: ''
    }
  },
  methods: {
    myUpload(bytes, { fileName }) {
      return uploadMergedPdfBytes(bytes, { fileName })
    },
    async mySubmit({ raw }) {
      await saveSignContract(this.openAccountId, raw)
      return { ok: true }
    },
    onComplete({ fileUrl, raw }) {
      uni.redirectTo({ url: '/pages/success/success' })
    },
    onFail({ stage, error }) {
      uni.showToast({ title: error.message || stage, icon: 'none' })
    }
  }
}
</script>

签署流程(用户视角)

  1. 打开页面 → 自动下载并渲染 PDF + 锚点
  2. 点击锚点或底栏「签署」→ 弹出 <SignPad />(内部 confirm-export="export-bytes",监听 @export-bytes
  3. 手写 → 确认 → 锚点框内显示签名预览
  4. 全部签完 → 底栏「提交签署」→ 合成 PDF → 上传 → submitFn(若有)→ complete

6. Core / Platform JS API

不依赖 UI 时,可直接使用底层模块(如 Node 脚本校验、自定义页面)。

6.1 contract-sign-core

import {
  parseSignPositionPayload,
  validateSignPosition,
  isAllAnchorsSigned,
  mergeSignaturesIntoPdf,
  base64ToUint8Array,
  resolveAnchorPdfRect
} from '@/packages/contract-sign-core/src/index.js'
函数 说明
parseSignPositionPayload(raw) 解析 signPosition 字符串或对象
validateSignPosition(raw) { ok, data }{ ok: false, error }
isAllAnchorsSigned(anchors, signedMap) 是否全部签完
mergeSignaturesIntoPdf(pdfBuf, signPosition, signedImages) 返回 Promise<Uint8Array>
base64ToUint8Array(base64) base64 → 二进制
resolveAnchorPdfRect(anchor, pages) 单锚点 pdf-lib 矩形

仅合成 PDF(无 UI)

const signPosition = parseSignPositionPayload(jsonFromBackend)
const signedImages = {
  a1: pngUint8Array  // 每个 anchorId 对应 PNG 二进制
}
const outBytes = await mergeSignaturesIntoPdf(pdfArrayBuffer, signPosition, signedImages)
// 验证:文件头应为 %PDF-(非 Base64 文本)

6.2 contract-sign-platform

import {
  downloadPdfAsArrayBuffer,
  resolvePdfDownloadUrl,
  createPdfDownloader,
  isValidPdfBytes,
  runContractSignSubmit,
  createSubmitConfig
} from '@/packages/contract-sign-platform/src/index.js'
函数 说明
downloadPdfAsArrayBuffer(url, options?) 下载 PDF 为 ArrayBuffer
resolvePdfDownloadUrl(url, options?) H5 代理 URL 转换
createPdfDownloader(options?) 工厂:返回 (url) => Promise<ArrayBuffer>
isValidPdfBytes(data) 校验 %PDF- 文件头
runContractSignSubmit({ outBytes, fileName, uploadFn, submitFn }) 上传 + 可选 submit 编排
createSubmitConfig(options?) 上传地址、表单字段等配置

下载 + 合成 + 上传(无 ContractSign 组件)

import { downloadPdfAsArrayBuffer, runContractSignSubmit } from '@/packages/contract-sign-platform/src/index.js'
import { parseSignPositionPayload, mergeSignaturesIntoPdf } from '@/packages/contract-sign-core/src/index.js'
import { uploadMergedPdfBytes } from '@/utils/contractPdfUpload.js'

const pdfBuf = await downloadPdfAsArrayBuffer(contract.filePath)
const signPosition = parseSignPositionPayload(contract.signPosition)
const outBytes = await mergeSignaturesIntoPdf(pdfBuf, signPosition, signedImages)

const { signedFileUrl, uploadRaw } = await runContractSignSubmit({
  outBytes,
  fileName: 'contract_signed.pdf',
  uploadFn: uploadMergedPdfBytes,
  submitFn: async ({ raw }) => {
    await myApi.saveContract(raw)
  }
})

7. 使用场景手册

场景 A:只要画板(SignPad)

项目 说明
组件 <SignPad />
是否需要 signPosition
是否需要 pdfUrl
产出物 base64:@export / exportPng();二进制:@export-bytes / exportPngBytes()
Demo /packages/contract-sign-demo/pages/demo/sign-pad-only

场景 B:只要 PDF 预览(PdfSignViewer)

项目 说明
组件 <PdfSignViewer view-only />
是否需要 signPosition 可选(有则显示锚点框)
是否需要 pdfUrl 是(或 pdfBytes
Demo /packages/contract-sign-demo/pages/demo/view-only

场景 C:PDF 预览 + 可点锚点(尚未接画板)

<PdfSignViewer
  :pdf-url="pdfUrl"
  :sign-position="signPosition"
  :view-only="false"
  @anchor-click="({ anchorId }) => openMyCustomSign(anchorId)"
/>

业务在 anchor-click 里自行弹 <SignPad /> 或跳转签字页,签完后更新 signedMap 传给 PdfSignViewer

场景 D:完整合同签署(ContractSign)

项目 说明
组件 <ContractSign />
必填 pdfUrl + signPosition
可选 uploadFnsubmitFn
监听 @complete
Demo /packages/contract-sign-demo/pages/demo/sign
本仓库业务页 pages/contract/contractSign.vue(薄封装)

场景 E:只读查看已签合同(无签署)

<ContractSign
  :pdf-url="signedUrl"
  :sign-position="signPosition"
  view-only
/>

或直接使用 <PdfSignViewer />(更轻量)。

场景 F:自己拼「预览 + 画板 + 合成」(高级)

  1. <PdfSignViewer :view-only="false" @anchor-click="..." />
  2. <SignPad confirm-export="export-bytes" @export-bytes="storeSign" />
  3. mergeSignaturesIntoPdf + runContractSignSubmit

一般 优先用 <ContractSign />,除非 UI 必须完全自定义。


8. 业务页薄封装模式

本仓库 pages/contract/contractSign.vue 为参考实现:

留在业务页 进组件库
路由参数、onLoad PDF 渲染、锚点、画板
api_getContract 拉合同 合成、默认上传
saveSimpleOpenAccountSignContract 落库 SignPadPdfSignViewer
跳转成功页、扫码 token 流程 mergeSignaturesIntoPdf
<!-- pages/contract/contractSign.vue 精简形态 -->
<template>
  <ContractSign
    v-if="pageReady"
    :pdf-url="pdfUrl"
    :sign-position="signPositionRaw"
    :view-only="viewOnly"
    :mode="signMode"
    :hide-top-nav="hideTopNav"
    :auto-show-upload-url="showUploadUrlModal"
    @back="goBack"
    @complete="onSignComplete"
  />
</template>

业务页负责:pageReadypdfUrlsignPositionRaw 赋值;onSignComplete 里落库与 redirectTo 成功页。


9. Demo 与本地调试

页面 路径 验证内容
SignPad packages/contract-sign-demo/pages/demo/sign-pad-only 画板、@export-bytes / exportPngBytes()
SignPad 测试 pages/contractSignTest/index.vue(画板 Tab) base64 与二进制双链路
PdfSignViewer packages/contract-sign-demo/pages/demo/view-only 演示 PDF + 锚点
ContractSign packages/contract-sign-demo/pages/demo/sign 完整签署
uni.navigateTo({
  url: '/packages/contract-sign-demo/pages/demo/sign'
})

演示数据来自 utils/contractSignDemo.js 中的 DEMO_CONTRACT_DETAIL(含 filePathsignPosition)。


10. H5 / App 差异与配置

H5 App
PDF 下载 可能需要 devServer /pdf-proxy 解决 CORS uni.request 直连
PDF 渲染 pdf.js + worker(同源 /static/pdf/... worker 用 _www/static/pdf/... 或 CDN
画板 renderjs canvas 同左
PDF 上传 fetch / XHR + FormData 优先 renderjs fetch+FormData;逻辑层 uploadMergedPdfBytes 回退

自定义 H5 PDF 代理

<ContractSign
  :pdf-url="pdfUrl"
  :sign-position="signPosition"
  :h5-pdf-proxy="myProxy"
/>

<script>
methods: {
  myProxy(url) {
    // 将 OSS URL 转为本地代理路径
    if (url.includes('my-oss.com')) {
      return '/my-pdf-proxy' + new URL(url).pathname
    }
    return url
  }
}
</script>

vue.config.js 示例(本仓库)

devServer: {
  proxy: {
    '/pdf-proxy': {
      target: 'https://zennerfile.maobaoinfo.cn',
      changeOrigin: true,
      pathRewrite: { '^/pdf-proxy': '' }
    }
  }
}

11. 常见问题

Q1:合成后的 PDF 打开乱码或显示 Base64 文本?

合成结果必须是 二进制 PDF(文件头 %PDF-),不是把 Base64 字符串写入文件。请使用 mergeSignaturesIntoPdf 返回的 Uint8Array 直接上传。

Q2:App 上画板点不了或坐标偏移?

必须使用组件库自带 <SignPad />(renderjs),不要改用 Vue 层 uni.createCanvasContext。横屏模式依赖 signLandscapeTouch.js,不要删掉 renderjs 模块。

Q3:锚点位置与 Web 不一致?

确认 signPosition 与 Web 营收配置为同一 JSON;组件库已兼容 nx/ny/nw/nhboxNormalized

Q4:viewOnly 下锚点能点击吗?

<PdfSignViewer view-only /> 默认 不可点击。若需点击但不签署,设 :view-only="false" 并监听 anchor-click

Q5:如何只上传不弹链接窗?

<ContractSign :auto-show-upload-url="false" @complete="onComplete" />

Q6:旧代码 import from utils/contractPdfSign.js 还能用吗?

可以。smartworkapp 内已 re-export 到 packages/contract-sign-core,建议新代码改用 packages 路径。

Q7:复制到其他项目还要带什么?

  • packages/contract-sign-core
  • packages/contract-sign-platform
  • packages/contract-sign-ui
  • 宿主 package.json 中的 pdf-libpdfjs-dist
  • static/pdf/pdf.worker.min.js
  • H5 代理配置(如需要)

附录:与本仓库文件对照

组件库 原 smartworkapp 路径(兼容 re-export)
contract-sign-core/coordinate.js utils/contractSignatureCoordinate.js
contract-sign-core/pdfMerge.js utils/contractPdfSign.js
contract-sign-platform/pdfDownload.js utils/contractPdfDownload.js
contract-sign-ui/SignPad contractSign.vue 内 renderjs 画板
contract-sign-ui/PdfSignViewer contractSign.vue 内 pdf 预览
contract-sign-ui/ContractSign contractSign.vue 整页能力

十二、发布为 uni_modules(P4)

团队内分发:在本仓库执行:

node scripts/sync-contract-sign-uni-modules.mjs

将生成 uni_modules/contract-sign-ui/,同事复制该文件夹到自己的项目即可。

完整步骤(安装、uploadFn、worker、easycom):contract-sign-uni-modules-publish.md


隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

许可协议

MIT协议

暂无用户评论。