更新记录
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
- 使用手册(场景 + 示例 + API 大全):
docs/contract-sign-user-guide.md - 抽取方案定稿:
docs/contract-sign-component-library.md第 3 节
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。
目录
- 功能概览
- 包结构与依赖
- 接入项目(5 分钟)
- signPosition 数据约定
- UI 组件 API
- Core / Platform JS API
- 使用场景手册
- 业务页薄封装模式
- Demo 与本地调试
- H5 / App 差异与配置
- 常见问题
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.json 中 pdfjs-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_modules:uni_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();bytes 为 Uint8Array |
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> 预览 |
@export 或 exportPng(),自行拼 data:image/png;base64, 前缀 |
| 上传 PNG / 写 PDF | @export-bytes 或 exportPngBytes(),直接用 bytes |
| 确认后自动关画板 | close-on-export 默认 true |
| 导出后继续编辑 | :close-on-export="false" |
| 监听是否有笔画 | @change="({ hasStroke }) => {}" |
注意
- ref 调用
exportPng()/exportPngBytes()时,画板须已打开且已有签名,否则 Promise reject。 - 画板全屏遮罩会挡住页面内按钮;可让用户点画板「确认」,或将操作按钮浮在遮罩之上(
z-index > 100000)。 - 空画布导出会 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>
签署流程(用户视角)
- 打开页面 → 自动下载并渲染 PDF + 锚点
- 点击锚点或底栏「签署」→ 弹出
<SignPad />(内部confirm-export="export-bytes",监听@export-bytes) - 手写 → 确认 → 锚点框内显示签名预览
- 全部签完 → 底栏「提交签署」→ 合成 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 |
| 可选 | uploadFn、submitFn |
| 监听 | @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:自己拼「预览 + 画板 + 合成」(高级)
<PdfSignViewer :view-only="false" @anchor-click="..." /><SignPad confirm-export="export-bytes" @export-bytes="storeSign" />mergeSignaturesIntoPdf+runContractSignSubmit
一般 优先用 <ContractSign />,除非 UI 必须完全自定义。
8. 业务页薄封装模式
本仓库 pages/contract/contractSign.vue 为参考实现:
| 留在业务页 | 进组件库 |
|---|---|
路由参数、onLoad |
PDF 渲染、锚点、画板 |
api_getContract 拉合同 |
合成、默认上传 |
saveSimpleOpenAccountSignContract 落库 |
SignPad、PdfSignViewer |
| 跳转成功页、扫码 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>
业务页负责:pageReady、pdfUrl、signPositionRaw 赋值;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(含 filePath、signPosition)。
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/nh 与 boxNormalized。
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-corepackages/contract-sign-platformpackages/contract-sign-ui- 宿主
package.json中的pdf-lib、pdfjs-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

收藏人数:
下载插件并导入HBuilderX
赞赏(1)
下载 10
赞赏 1
下载 12430231
赞赏 1934
赞赏
京公网安备:11010802035340号