mirror of
https://gitee.com/wot-design-uni/wot-design-uni.git
synced 2025-12-06 17:18:40 +08:00
feat: ✨ 重构 Signature 签字板历史记录模式并添加压感模式和横屏示例 (#967)
This commit is contained in:
parent
8d9e5c6658
commit
aad3e8332b
@ -1,83 +1,113 @@
|
|||||||
# Signature 签名 <el-tag text style="vertical-align: middle;margin-left:8px;" effect="plain">1.6.0</el-tag>
|
# Signature 签名 <el-tag text style="vertical-align: middle;margin-left:8px;" effect="plain">1.6.0</el-tag>
|
||||||
|
|
||||||
用于签名场景,基于 Canvas 实现的签名组件。它提供了多种自定义选项,包括签名笔的颜色、宽度以及自定义操作按钮。
|
用于签名场景,基于 Canvas 实现的签名组件。提供了基础签名、历史记录、笔锋效果等功能。
|
||||||
|
|
||||||
:::tip 提醒
|
:::tip 提醒
|
||||||
如果遇到导出图片不清晰,可以将`exportScale`设置为`2`以上。
|
如果遇到导出图片不清晰,可以将 `exportScale` 设置为 `2` 以上。
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## 基础用法
|
## 基础用法
|
||||||
|
|
||||||
|
基础的电子签名功能。签名完成后会使用预览组件显示签名图片。
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<wd-signature @submit="confirm" @clear="clear" />
|
<wd-signature @confirm="confirm" @clear="clear" :export-scale="2" background-color="#ffffff" />
|
||||||
<wd-img :height="img.height" :width="img.width" :src="img.src" v-if="img.src" />
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const img = ref({
|
const img = ref<Partial<SignatureResult>>({})
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
src: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
function confirm(result: SignatureResult) {
|
function confirm(result: SignatureResult) {
|
||||||
img.value.src = result.tempFilePath
|
if (result.success) {
|
||||||
img.value.height = result.height
|
uni.previewImage({
|
||||||
img.value.width = result.width
|
urls: [result.tempFilePath]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
img.value = {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 开启历史记录
|
## 历史记录
|
||||||
|
|
||||||
|
通过 `enable-history` 开启历史记录功能,可以进行撤销和恢复操作。
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<wd-signature :history="true" background-color="lightgray" />
|
<wd-signature enable-history background-color="#f5f5f5" />
|
||||||
```
|
```
|
||||||
|
|
||||||
## 自定义画笔颜色
|
## 笔锋模式
|
||||||
|
|
||||||
`pen-color`设置签名笔的颜色,默认为`黑色`。
|
通过 `pressure` 开启笔锋模式,模拟真实书写效果。笔锋模式下笔画粗细会随书写速度变化。
|
||||||
|
|
||||||
|
### 基础笔锋效果
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<wd-signature pen-color="red" />
|
<wd-signature pressure :height="300" />
|
||||||
```
|
```
|
||||||
|
|
||||||
## 自定义画笔宽度
|
:::tip 使用建议
|
||||||
|
1. 笔锋模式推荐参数范围:
|
||||||
|
- min-width: 1-2
|
||||||
|
- max-width: 4-6
|
||||||
|
- min-speed: 1-2
|
||||||
|
2. max-width 和 min-width 的差值建议保持在 3-5 之间
|
||||||
|
3. min-speed 值越小,压感越灵敏,建议根据实际书写习惯调整
|
||||||
|
4. 对于签名场景,建议将画布高度设置在 300-400 之间
|
||||||
|
:::
|
||||||
|
|
||||||
`line-width`设置签名笔的宽度,默认为`2`。
|
### 自定义笔锋参数
|
||||||
|
|
||||||
|
可以通过以下属性精确控制笔锋效果:
|
||||||
|
- `min-width`: 最小笔画宽度,快速书写时的线条粗细
|
||||||
|
- `max-width`: 最大笔画宽度,慢速书写时的线条粗细
|
||||||
|
- `min-speed`: 速度阈值,用于调整压感灵敏度
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<wd-signature :line-width="6" />
|
<wd-signature
|
||||||
|
pressure
|
||||||
|
:height="300"
|
||||||
|
:min-width="1"
|
||||||
|
:max-width="6"
|
||||||
|
:min-speed="1.5"
|
||||||
|
background-color="#f5f5f5"
|
||||||
|
/>
|
||||||
|
<view class="tip-text">快速书写产生细线条,慢速书写产生粗线条</view>
|
||||||
```
|
```
|
||||||
|
|
||||||
## 自定义背景色
|
### 笔锋模式 + 历史记录
|
||||||
|
|
||||||
`background-color`设置画板的背景色,无默认值。
|
笔锋模式可以与历史记录功能结合使用,支持对带有笔锋效果的线条进行撤销和恢复操作。
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<wd-signature background-color="lightgray" />
|
<wd-signature
|
||||||
|
pressure
|
||||||
|
enable-history
|
||||||
|
:height="300"
|
||||||
|
:min-width="1"
|
||||||
|
:max-width="6"
|
||||||
|
background-color="#f5f5f5"
|
||||||
|
/>
|
||||||
|
<view class="tip-text">结合历史记录,支持笔锋效果的撤销与恢复</view>
|
||||||
```
|
```
|
||||||
|
|
||||||
## 禁用滚动
|
## 自定义功能
|
||||||
|
|
||||||
`disable-scroll`设置是否禁用画布滚动,默认为`true`。
|
### 自定义按钮
|
||||||
|
|
||||||
|
通过 `footer` 插槽自定义底部按钮。
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<wd-signature :disable-scroll="false" />
|
<wd-signature :disabled="disabled" enable-history :step="3">
|
||||||
```
|
<template #footer="{ clear, confirm, currentStep, restore, revoke, historyList }">
|
||||||
|
|
||||||
## 自定义按钮
|
|
||||||
|
|
||||||
通过`footer`插槽可以自定义按钮。
|
|
||||||
|
|
||||||
```html
|
|
||||||
<wd-signature :disabled="disabled" :step="3">
|
|
||||||
<template #footer="{ clear, confirm, currentStep, restore, revoke,historyList }">
|
|
||||||
<wd-button block @click="changeDisabled" v-if="disabled">开始签名</wd-button>
|
<wd-button block @click="changeDisabled" v-if="disabled">开始签名</wd-button>
|
||||||
<block v-if="!disabled">
|
<block v-if="!disabled">
|
||||||
<wd-button size="small" plain @click="revoke()" :disabled="currentStep <= 0">撤回三步</wd-button>
|
<wd-button size="small" plain @click="revoke" :disabled="currentStep <= 0">撤回</wd-button>
|
||||||
<wd-button size="small" plain @click="restore()" :disabled="!(currentStep < historyList.length)">恢复三步</wd-button>
|
<wd-button size="small" plain @click="restore" :disabled="currentStep >= historyList.length">恢复</wd-button>
|
||||||
<wd-button size="small" plain @click="clear">清除</wd-button>
|
<wd-button size="small" plain @click="clear">清除</wd-button>
|
||||||
<wd-button size="small" style="margin-left: 4px" @click="confirm">确认</wd-button>
|
<wd-button size="small" @click="confirm">确定</wd-button>
|
||||||
</block>
|
</block>
|
||||||
</template>
|
</template>
|
||||||
</wd-signature>
|
</wd-signature>
|
||||||
@ -91,48 +121,372 @@ function changeDisabled() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 自定义画笔
|
||||||
|
|
||||||
|
可以自定义画笔的颜色和宽度。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<wd-signature pen-color="#0083ff" :line-width="4" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 弹窗中使用
|
||||||
|
|
||||||
|
结合 `wd-popup` 组件在弹窗中使用签名板。建议使用 `after-enter` 事件调用签名板的 `init` 方法以确保正确初始化。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<wd-button type="primary" @click="show = true">打开签名板</wd-button>
|
||||||
|
|
||||||
|
<wd-popup
|
||||||
|
v-model="show"
|
||||||
|
closable
|
||||||
|
safe-area-inset-bottom
|
||||||
|
position="bottom"
|
||||||
|
custom-style="padding: 48px 20px 20px 20px; border-radius: 16px 16px 0 0;"
|
||||||
|
@after-enter="signatureRef?.init()"
|
||||||
|
>
|
||||||
|
<wd-signature
|
||||||
|
ref="signatureRef"
|
||||||
|
:height="300"
|
||||||
|
enable-history
|
||||||
|
pressure
|
||||||
|
background-color="#f5f5f5"
|
||||||
|
@confirm="handleConfirm"
|
||||||
|
/>
|
||||||
|
</wd-popup>
|
||||||
|
|
||||||
|
<wd-img v-if="img.tempFilePath" mode="widthFix" width="100%" :src="img.tempFilePath" />
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import type { SignatureInstance, SignatureResult } from '@/uni_modules/wot-design-uni/components/wd-signature/types'
|
||||||
|
|
||||||
|
const show = ref(false)
|
||||||
|
const img = ref<Partial<SignatureResult>>({})
|
||||||
|
const signatureRef = ref<SignatureInstance>()
|
||||||
|
|
||||||
|
function handleConfirm(result: SignatureResult) {
|
||||||
|
show.value = false
|
||||||
|
if (result.success) {
|
||||||
|
uni.previewImage({
|
||||||
|
urls: [result.tempFilePath]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```scss
|
||||||
|
.popup-footer {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::tip 提示
|
||||||
|
弹窗中使用签名板时,建议:
|
||||||
|
1. 开启 `closable` 显示关闭按钮
|
||||||
|
2. 设置 `safe-area-inset-bottom` 以适配底部安全区
|
||||||
|
3. 使用 `custom-style` 调整弹窗内边距,为关闭按钮留出空间
|
||||||
|
4. 在弹窗的 `after-enter` 事件中调用签名板的 `init` 方法,确保正确初始化
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 横屏签名页面
|
||||||
|
|
||||||
|
可以通过配置页面的 `pageOrientation` 来实现横屏签名页面。
|
||||||
|
|
||||||
|
```json
|
||||||
|
// pages.json
|
||||||
|
{
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "pages/signature-landscape/Index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "横屏签名",
|
||||||
|
"pageOrientation": "landscape"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<view class="landscape-signature">
|
||||||
|
<wd-signature
|
||||||
|
ref="signatureRef"
|
||||||
|
:height="height"
|
||||||
|
:width="width"
|
||||||
|
pressure
|
||||||
|
enable-history
|
||||||
|
background-color="#f5f5f5"
|
||||||
|
@confirm="handleConfirm"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const height = ref(0)
|
||||||
|
const width = ref(0)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const { windowWidth, windowHeight } = uni.getSystemInfoSync()
|
||||||
|
// 减去页面边距
|
||||||
|
height.value = windowWidth - 40
|
||||||
|
width.value = windowHeight - 40
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.landscape-signature {
|
||||||
|
padding: 20px;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
:::tip 提示
|
||||||
|
横屏签名页面的建议:
|
||||||
|
1. 使用 `pageOrientation: "landscape"` 强制横屏显示
|
||||||
|
2. 动态计算画布尺寸以适配不同设备
|
||||||
|
3. 注意横屏时 windowWidth 和 windowHeight 的对调
|
||||||
|
4. 建议开启笔锋模式提供更好的签名体验
|
||||||
|
:::
|
||||||
|
|
||||||
|
### 横屏签名
|
||||||
|
|
||||||
|
支持以下两种横屏签名实现方案:
|
||||||
|
|
||||||
|
#### 1. 通用横屏方案 (推荐)
|
||||||
|
|
||||||
|
通过自定义布局和按钮旋转实现横屏效果,适用于所有平台。
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<view class="landscape-signature">
|
||||||
|
<wd-signature
|
||||||
|
v-if="inited"
|
||||||
|
:height="height"
|
||||||
|
:width="width"
|
||||||
|
enable-history
|
||||||
|
pressure
|
||||||
|
background-color="#f5f5f5"
|
||||||
|
@confirm="handleConfirm"
|
||||||
|
>
|
||||||
|
<template #footer="{ clear, confirm, restore, revoke, canUndo, canRedo }">
|
||||||
|
<view class="custom-actions">
|
||||||
|
<view class="button-group">
|
||||||
|
<wd-button size="small" plain @click="revoke" :disabled="!canUndo">撤回</wd-button>
|
||||||
|
<wd-button size="small" plain @click="restore" :disabled="!canRedo">恢复</wd-button>
|
||||||
|
<wd-button size="small" plain @click="clear">清除</wd-button>
|
||||||
|
<wd-button size="small" type="primary" @click="confirm">完成</wd-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</wd-signature>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { pause } from '@/uni_modules/wot-design-uni/components/common/util'
|
||||||
|
|
||||||
|
const height = ref(0)
|
||||||
|
const width = ref(0)
|
||||||
|
const inited = ref(false)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const { windowWidth, windowHeight } = uni.getSystemInfoSync()
|
||||||
|
width.value = windowWidth - 48
|
||||||
|
height.value = windowHeight - 48
|
||||||
|
|
||||||
|
pause(100).then(() => {
|
||||||
|
inited.value = true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```scss
|
||||||
|
.landscape-signature {
|
||||||
|
height: 100vh;
|
||||||
|
// #ifdef H5
|
||||||
|
height: calc(100vh - 44px);
|
||||||
|
// #endif
|
||||||
|
background: #fff;
|
||||||
|
position: relative;
|
||||||
|
padding: 24px 0;
|
||||||
|
padding-left: 48px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.custom-actions {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
width: 48px;
|
||||||
|
transform: translateY(-50%) rotate(90deg);
|
||||||
|
transform-origin: center;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: max-content;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::tip 实现说明
|
||||||
|
通用横屏方案特点:
|
||||||
|
1. 使用 fixed 布局配合旋转实现左侧垂直按钮栏
|
||||||
|
2. 通过 footer 插槽自定义操作按钮
|
||||||
|
3. 使用 transform 实现按钮的旋转效果
|
||||||
|
4. 适用于所有平台,实现方式一致
|
||||||
|
5. 建议使用 inited 变量配合延迟加载避免画布初始化问题
|
||||||
|
:::
|
||||||
|
|
||||||
|
#### 2. 原生横屏方案 (仅微信小程序)
|
||||||
|
|
||||||
|
微信小程序提供了原生的横屏支持,使用时需要注意区分平台:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"path": "pages/signature/landscape",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "横屏签名",
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
"pageOrientation": "landscape"
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<view class="landscape-signature">
|
||||||
|
<wd-signature
|
||||||
|
v-if="inited"
|
||||||
|
ref="signatureRef"
|
||||||
|
:height="height"
|
||||||
|
:width="width"
|
||||||
|
enable-history
|
||||||
|
pressure
|
||||||
|
background-color="#f5f5f5"
|
||||||
|
@confirm="handleConfirm"
|
||||||
|
>
|
||||||
|
</wd-signature>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { pause } from '@/uni_modules/wot-design-uni/components/common/util'
|
||||||
|
|
||||||
|
const height = ref(0)
|
||||||
|
const width = ref(0)
|
||||||
|
const inited = ref(false)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const { windowWidth, windowHeight } = uni.getSystemInfoSync()
|
||||||
|
width.value = windowWidth
|
||||||
|
height.value = windowHeight - 60 // 预留底部按钮空间
|
||||||
|
|
||||||
|
pause(100).then(() => {
|
||||||
|
inited.value = true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```scss
|
||||||
|
.landscape-signature {
|
||||||
|
height: 100vh;
|
||||||
|
background: #fff;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.weixin-actions {
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::warning 注意事项
|
||||||
|
1. `pageOrientation` 配置仅在微信小程序端生效
|
||||||
|
2. 使用条件编译区分不同平台的布局结构
|
||||||
|
3. 微信小程序页面会自动旋转,按钮布局不需要特殊处理
|
||||||
|
4. 预留底部按钮空间时需要考虑横屏布局
|
||||||
|
5. 其他平台请使用通用横屏方案
|
||||||
|
:::
|
||||||
|
|
||||||
## Attributes
|
## Attributes
|
||||||
|
|
||||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 | 最低版本 |
|
| 参数 | 说明 | 类型 | 默认值 | 最低版本 |
|
||||||
|-----------------|----------------------------------------------------------------------|---------|--------|----------|----------|
|
|------|------|------|--------|----------|
|
||||||
| penColor | 签名笔颜色 | String | -- | #000000 | -- |
|
| pen-color | 签名笔颜色 | string | #000000 | - |
|
||||||
| lineWidth | 签名笔宽度 | Number | -- | 2 | -- |
|
| line-width | 签名笔宽度 | number | 3 | - |
|
||||||
| height | 画布的高度 | Number | -- | 200 | -- |
|
| height | 画布的高度 | number | 200 | - |
|
||||||
| width | 画布的宽度 | Number | -- | 300 | -- |
|
| width | 画布的宽度 | number | 300 | - |
|
||||||
| clearText | 清空按钮的文本 | String | -- | 清空 | -- |
|
| clear-text | 清空按钮的文本 | string | - | - |
|
||||||
| confirmText | 确认按钮的文本 | String | -- | 确认 | -- |
|
| confirm-text | 确认按钮的文本 | string | - | - |
|
||||||
| fileType | 目标文件的类型,[uni.canvasToTempFilePath属性介绍](https://uniapp.dcloud.net.cn/api/canvas/canvasToTempFilePath.html) | String | -- | png | -- |
|
| file-type | 导出图片类型 | string | png | - |
|
||||||
| quality | 图片的质量,取值范围为 `(0, 1]`,不在范围内时当作1.0处理,[uni.canvasToTempFilePath属性介绍](https://uniapp.dcloud.net.cn/api/canvas/canvasToTempFilePath.html) | Number | -- | 1 | -- |
|
| quality | 导出图片质量(0-1) | number | 1 | - |
|
||||||
| exportScale | 导出图片的缩放比例 | Number | -- | 1 | -- |
|
| export-scale | 导出图片的缩放比例 | number | 1 | - |
|
||||||
| disabled | 是否禁用签名板 | Boolean | -- | false | -- |
|
| disabled | 是否禁用签名板 | boolean | false | - |
|
||||||
| backgroundColor | 画板的背景色 | String | -- | -- | -- |
|
| background-color | 画板的背景色 | string | - | - |
|
||||||
| disableScroll | 是否禁用画布滚动 | Boolean | -- | true | -- |
|
| disable-scroll | 是否禁用画布滚动 | boolean | true | - |
|
||||||
| history | 是否开启历史记录 | Boolean | -- | false | -- |
|
| enable-history | 是否开启历史记录 | boolean | false | - |
|
||||||
| step | 开启历史记录之后的步长(撤回step步) | Number | -- | 1 | -- |
|
| step | 历史记录步长 | number | 1 | - |
|
||||||
|
| pressure | 是否启用笔锋模式 | boolean | false | - |
|
||||||
## Slot
|
| min-width | 笔锋模式最小宽度 | number | 2 | - |
|
||||||
|
| max-width | 笔锋模式最大宽度 | number | 6 | - |
|
||||||
| name | 说明 | 参数 | 最低版本 |
|
| min-speed | 笔锋模式速度阈值 | number | 1.5 | - |
|
||||||
|--------|----------------|-----------------------|----------|
|
|
||||||
| footer | 自定义footer | `{ clear, confirm, restore, revoke, currentStep ,historyList }` | - |
|
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
| 事件名称 | 说明 | 参数 | 最低版本 |
|
| 事件名称 | 说明 | 参数 | 最低版本 |
|
||||||
|-----------|--------------------|-------------------------------------------|----------|
|
|---------|------|------|----------|
|
||||||
| start | 开始签名时触发 | - | - |
|
| start | 开始签名时触发 | event: TouchEvent | - |
|
||||||
| end | 结束签名时触发 | - | - |
|
| end | 结束签名时触发 | event: TouchEvent | - |
|
||||||
| signing | 签名过程中触发 | `event` | - |
|
| signing | 签名过程中触发 | event: TouchEvent | - |
|
||||||
| confirm | 点击确定按钮时触发 | `{tempFilePath, width, height, success}` 分别为生成文件的临时路径 (本地路径)、生成图片宽、生成图片高、是否成功 | - |
|
| confirm | 确认签名时触发 | result: SignatureResult | - |
|
||||||
| clear | 点击清空按钮时触发 | - | - |
|
| clear | 清空签名时触发 | - | - |
|
||||||
|
|
||||||
## Methods
|
## Methods
|
||||||
|
|
||||||
对外暴露函数
|
| 方法名 | 说明 | 参数 | 最低版本 |
|
||||||
|
|--------|------|------|----------|
|
||||||
|
| init | 初始化签名板 | forceUpdate?: boolean | - |
|
||||||
|
| confirm | 确认签名 | - | - |
|
||||||
|
| clear | 清空签名 | - | - |
|
||||||
|
| restore | 恢复上一步 | - | - |
|
||||||
|
| revoke | 撤销上一步 | - | - |
|
||||||
|
|
||||||
| 事件名称 | 说明 | 参数 | 最低版本 |
|
## Slots
|
||||||
|-----------|--------------------|-------------------------------------------|----------|
|
|
||||||
| confirm | 点击确认按钮时触发 | `{tempFilePath, width, height, success}` 分别为生成文件的临时路径 (本地路径)、生成图片宽、生成图片高、是否成功 | - |
|
| 名称 | 说明 | 参数 | 最低版本 |
|
||||||
| clear | 点击清空按钮时触发 | - | - |
|
|------|------|------|----------|
|
||||||
| restore | 暴露恢复方法 | - | - |
|
| footer | 自定义底部按钮 | `{ clear, confirm, restore, revoke, currentStep, historyList }` | - |
|
||||||
| revoke | 暴露撤回方法 | - | - |
|
|
||||||
|
|||||||
@ -811,6 +811,17 @@
|
|||||||
"navigationBarTitleText": "Signature 签名"
|
"navigationBarTitleText": "Signature 签名"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/signature/Landscape",
|
||||||
|
"name": "signatureLandscape",
|
||||||
|
"style": {
|
||||||
|
"mp-alipay": {
|
||||||
|
"allowsBounceVertical": "NO"
|
||||||
|
},
|
||||||
|
"navigationBarTitleText": "横屏签名",
|
||||||
|
"pageOrientation": "landscape"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/backtop/Index",
|
"path": "pages/backtop/Index",
|
||||||
"name": "backtop",
|
"name": "backtop",
|
||||||
|
|||||||
@ -1,51 +1,76 @@
|
|||||||
<!--
|
<!--
|
||||||
* @Author: 810505339
|
* @Author: 810505339
|
||||||
* @Date: 2025-02-11 21:17:21
|
* @Date: 2025-02-11 21:17:21
|
||||||
* @LastEditors: 810505339
|
* @LastEditors: weisheng
|
||||||
<<<<<<< HEAD
|
* @LastEditTime: 2025-03-23 14:12:32
|
||||||
* @LastEditTime: 2025-02-14 12:22:03
|
* @FilePath: /wot-design-uni/src/pages/signature/Index.vue
|
||||||
=======
|
|
||||||
* @LastEditTime: 2025-02-15 21:38:52
|
|
||||||
>>>>>>> 4c29deae524fe910351cc9a131067fb6124891b7
|
|
||||||
* @FilePath: \wot-design-uni\src\pages\signature\Index.vue
|
|
||||||
* 记得注释
|
* 记得注释
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<page-wraper>
|
<page-wraper>
|
||||||
<demo-block title="基础用法">
|
<demo-block title="基础用法">
|
||||||
<wd-signature @confirm="confirm" @clear="clear" :export-scale="2" />
|
<wd-signature @confirm="confirm" @clear="clear" :export-scale="2" background-color="#ffffff" />
|
||||||
<wd-img v-if="img.tempFilePath" mode="widthFix" width="100%" :src="img.tempFilePath" />
|
|
||||||
</demo-block>
|
</demo-block>
|
||||||
<demo-block title="开启历史记录">
|
|
||||||
<wd-signature :history="true" background-color="lightgray" />
|
<demo-block title="历史记录">
|
||||||
|
<wd-signature enable-history background-color="#f5f5f5" />
|
||||||
</demo-block>
|
</demo-block>
|
||||||
<demo-block title="自定义画笔颜色">
|
|
||||||
<wd-signature pen-color="red" />
|
<demo-block title="笔锋模式-基础">
|
||||||
|
<wd-signature pressure :height="300" />
|
||||||
</demo-block>
|
</demo-block>
|
||||||
<demo-block title="自定义画笔宽度">
|
|
||||||
<wd-signature :line-width="6" />
|
<demo-block title="笔锋模式-自定义">
|
||||||
|
<wd-signature pressure :height="300" :min-width="1" :max-width="6" :min-speed="1.5" background-color="#f5f5f5" />
|
||||||
|
<view class="tip-text">快速书写产生细线条,慢速书写产生粗线条</view>
|
||||||
</demo-block>
|
</demo-block>
|
||||||
<demo-block title="自定义背景颜色">
|
|
||||||
<wd-signature background-color="lightgray" />
|
<demo-block title="笔锋模式 + 历史记录">
|
||||||
|
<wd-signature pressure enable-history :height="300" :min-width="1" :max-width="6" background-color="#f5f5f5" />
|
||||||
|
<view class="tip-text">结合历史记录,支持笔锋效果的撤销与恢复</view>
|
||||||
</demo-block>
|
</demo-block>
|
||||||
<demo-block title="自定义插槽">
|
|
||||||
<wd-signature :disabled="disabled" :history="true" :step="3">
|
<demo-block title="自定义按钮">
|
||||||
|
<wd-signature :disabled="disabled" enable-history :step="3">
|
||||||
<template #footer="{ clear, confirm, currentStep, restore, revoke, historyList }">
|
<template #footer="{ clear, confirm, currentStep, restore, revoke, historyList }">
|
||||||
<wd-button block @click="changeDisabled" v-if="disabled">开始签名</wd-button>
|
<wd-button block @click="changeDisabled" v-if="disabled">开始签名</wd-button>
|
||||||
<block v-if="!disabled">
|
<block v-if="!disabled">
|
||||||
<wd-button size="small" plain @click="revoke()" :disabled="currentStep <= 0">撤回三步</wd-button>
|
<wd-button size="small" plain @click="revoke" :disabled="currentStep <= 0">撤回</wd-button>
|
||||||
<wd-button size="small" plain @click="restore()" :disabled="!(currentStep < historyList.length)">恢复三步</wd-button>
|
<wd-button size="small" plain @click="restore" :disabled="currentStep >= historyList.length">恢复</wd-button>
|
||||||
<wd-button size="small" plain @click="clear">清除</wd-button>
|
<wd-button size="small" plain @click="clear">清除</wd-button>
|
||||||
<wd-button size="small" style="margin-left: 4px" @click="confirm">确定</wd-button>
|
<wd-button size="small" @click="confirm">确定</wd-button>
|
||||||
</block>
|
</block>
|
||||||
</template>
|
</template>
|
||||||
</wd-signature>
|
</wd-signature>
|
||||||
</demo-block>
|
</demo-block>
|
||||||
|
|
||||||
|
<demo-block title="自定义画笔">
|
||||||
|
<wd-signature pen-color="#0083ff" :line-width="4" />
|
||||||
|
</demo-block>
|
||||||
|
|
||||||
|
<demo-block title="弹窗中使用">
|
||||||
|
<wd-button type="primary" @click="showPopup = true">打开签名板</wd-button>
|
||||||
|
|
||||||
|
<wd-popup
|
||||||
|
v-model="showPopup"
|
||||||
|
closable
|
||||||
|
safe-area-inset-bottom
|
||||||
|
position="bottom"
|
||||||
|
custom-style="padding: 48px 20px 20px 20px; border-radius: 16px 16px 0 0;"
|
||||||
|
@after-enter="signatureRef?.init()"
|
||||||
|
>
|
||||||
|
<wd-signature ref="signatureRef" :height="300" enable-history pressure background-color="#f5f5f5" @confirm="handlePopupConfirm" />
|
||||||
|
</wd-popup>
|
||||||
|
</demo-block>
|
||||||
|
|
||||||
|
<demo-block title="横屏签名">
|
||||||
|
<wd-button type="primary" @click="toSignatureLandscape">使用横屏签名</wd-button>
|
||||||
|
</demo-block>
|
||||||
</page-wraper>
|
</page-wraper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { SignatureResult } from '@/uni_modules/wot-design-uni/components/wd-signature/types'
|
import type { SignatureInstance, SignatureResult } from '@/uni_modules/wot-design-uni/components/wd-signature/types'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const img = ref<Partial<SignatureResult>>({})
|
const img = ref<Partial<SignatureResult>>({})
|
||||||
@ -53,7 +78,12 @@ const img = ref<Partial<SignatureResult>>({})
|
|||||||
const disabled = ref(true)
|
const disabled = ref(true)
|
||||||
|
|
||||||
function confirm(result: SignatureResult) {
|
function confirm(result: SignatureResult) {
|
||||||
img.value = result
|
debugger
|
||||||
|
if (result.success) {
|
||||||
|
uni.previewImage({
|
||||||
|
urls: [result.tempFilePath]
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
@ -63,4 +93,53 @@ function clear() {
|
|||||||
function changeDisabled() {
|
function changeDisabled() {
|
||||||
disabled.value = false
|
disabled.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showPopup = ref(false)
|
||||||
|
const signatureRef = ref<SignatureInstance>()
|
||||||
|
|
||||||
|
function handlePopupConfirm(result: SignatureResult) {
|
||||||
|
showPopup.value = false
|
||||||
|
if (result.success) {
|
||||||
|
uni.previewImage({
|
||||||
|
urls: [result.tempFilePath]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSignatureLandscape() {
|
||||||
|
console.log(232)
|
||||||
|
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/signature/Landscape'
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tip-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.button-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-footer {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
109
src/pages/signature/Landscape.vue
Normal file
109
src/pages/signature/Landscape.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<view class="landscape-signature">
|
||||||
|
<wd-signature
|
||||||
|
v-if="inited"
|
||||||
|
ref="signatureRef"
|
||||||
|
:height="height"
|
||||||
|
:width="width"
|
||||||
|
enable-history
|
||||||
|
pressure
|
||||||
|
background-color="#f5f5f5"
|
||||||
|
@confirm="handleConfirm"
|
||||||
|
>
|
||||||
|
<!-- #ifndef MP-WEIXIN -->
|
||||||
|
|
||||||
|
<template #footer="{ clear, confirm, restore, revoke, canUndo, canRedo }">
|
||||||
|
<view class="custom-actions">
|
||||||
|
<view class="button-group">
|
||||||
|
<wd-button size="small" plain @click="revoke" :disabled="!canUndo">撤回</wd-button>
|
||||||
|
<wd-button size="small" plain @click="restore" :disabled="!canRedo">恢复</wd-button>
|
||||||
|
<wd-button size="small" plain @click="clear">清除</wd-button>
|
||||||
|
<wd-button size="small" type="primary" @click="confirm">完成</wd-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<!-- #endif -->
|
||||||
|
</wd-signature>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { pause } from '@/uni_modules/wot-design-uni/components/common/util'
|
||||||
|
import type { SignatureInstance, SignatureResult } from '@/uni_modules/wot-design-uni/components/wd-signature/types'
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const signatureRef = ref<SignatureInstance>()
|
||||||
|
const height = ref(0)
|
||||||
|
const width = ref(0)
|
||||||
|
const inited = ref(false)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const { windowWidth, windowHeight } = uni.getSystemInfoSync()
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
// 微信小程序下不需要预留按钮空间,使用全屏尺寸
|
||||||
|
width.value = windowWidth
|
||||||
|
height.value = windowHeight - 60 // 预留底部按钮空间
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
width.value = windowWidth - 48
|
||||||
|
height.value = windowHeight - 48
|
||||||
|
inited.value = true
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
pause(100).then(() => {
|
||||||
|
inited.value = true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleConfirm(result: SignatureResult) {
|
||||||
|
if (result.success) {
|
||||||
|
uni.previewImage({
|
||||||
|
urls: [result.tempFilePath]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.landscape-signature {
|
||||||
|
height: 100vh;
|
||||||
|
// #ifdef H5
|
||||||
|
height: calc(100vh - 44px);
|
||||||
|
// #endif
|
||||||
|
background: #fff;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
// 微信小程序横屏模式下的布局
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
padding: 24px 0;
|
||||||
|
padding-left: 48px;
|
||||||
|
|
||||||
|
.custom-actions {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
width: 48px;
|
||||||
|
transform: translateY(-50%) rotate(90deg);
|
||||||
|
transform-origin: center;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: max-content;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,11 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
* @Author: 810505339
|
* @Author: 810505339
|
||||||
* @Date: 2025-01-10 20:03:57
|
* @Date: 2025-01-10 20:03:57
|
||||||
* @LastEditors: 810505339
|
* @LastEditors: weisheng
|
||||||
* @LastEditTime: 2025-02-18 13:04:45
|
* @LastEditTime: 2025-03-23 16:35:14
|
||||||
* @FilePath: \wot-design-uni\src\uni_modules\wot-design-uni\components\wd-signature\types.ts
|
* @FilePath: /wot-design-uni/src/uni_modules/wot-design-uni/components/wd-signature/types.ts
|
||||||
* 记得注释
|
* 记得注释
|
||||||
*/
|
*/
|
||||||
|
import type { ComponentPublicInstance, ExtractPropTypes } from 'vue'
|
||||||
import { baseProps, numericProp } from '../common/props'
|
import { baseProps, numericProp } from '../common/props'
|
||||||
|
|
||||||
export const signatureProps = {
|
export const signatureProps = {
|
||||||
@ -108,31 +109,155 @@ export const signatureProps = {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
/* 是否开始历史记录 */
|
/**
|
||||||
history: {
|
* 是否开启历史记录
|
||||||
|
* 类型:boolean
|
||||||
|
* 默认值:false
|
||||||
|
*/
|
||||||
|
enableHistory: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 撤回和恢复的步长
|
||||||
|
* 类型:number
|
||||||
|
* 默认值:1
|
||||||
|
*/
|
||||||
step: {
|
step: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1
|
default: 1
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 撤回按钮的文本
|
||||||
|
* 类型:string
|
||||||
|
* 默认值:撤销
|
||||||
|
*/
|
||||||
undoText: String,
|
undoText: String,
|
||||||
redoText: String
|
/**
|
||||||
|
* 恢复按钮的文本
|
||||||
|
* 类型:string
|
||||||
|
* 默认值:恢复
|
||||||
|
*/
|
||||||
|
redoText: String,
|
||||||
|
/**
|
||||||
|
* 是否启用压感模式(笔锋)
|
||||||
|
* 类型:boolean
|
||||||
|
* 默认值:false
|
||||||
|
*/
|
||||||
|
pressure: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 压感模式下笔画最小宽度
|
||||||
|
* 类型:number
|
||||||
|
* 默认值:2
|
||||||
|
*/
|
||||||
|
minWidth: {
|
||||||
|
type: Number,
|
||||||
|
default: 2
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 压感模式下笔画最大宽度
|
||||||
|
* 类型:number
|
||||||
|
* 默认值:6
|
||||||
|
*/
|
||||||
|
maxWidth: {
|
||||||
|
type: Number,
|
||||||
|
default: 6
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 最小速度阈值,影响压感模式下的笔画宽度变化
|
||||||
|
* 类型:number
|
||||||
|
* 默认值:1.5
|
||||||
|
*/
|
||||||
|
minSpeed: {
|
||||||
|
type: Number,
|
||||||
|
default: 1.5
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名结果类型
|
||||||
|
* @property tempFilePath - 生成图片的临时路径
|
||||||
|
* @property success - 是否成功生成图片
|
||||||
|
* @property width - 生成图片的宽度
|
||||||
|
* @property height - 生成图片的高度
|
||||||
|
*/
|
||||||
export type SignatureResult = {
|
export type SignatureResult = {
|
||||||
tempFilePath: string
|
tempFilePath: string
|
||||||
success: boolean
|
success: boolean
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名线条类型
|
||||||
|
* @property points - 线条所包含的所有点的数组
|
||||||
|
* @property color - 线条颜色
|
||||||
|
* @property width - 线条宽度
|
||||||
|
* @property backgroundColor - 线条背景色 (可选)
|
||||||
|
* @property isPressure - 是否为笔锋模式的线条 (可选)
|
||||||
|
*/
|
||||||
|
export interface Line {
|
||||||
|
points: Point[]
|
||||||
|
color: string
|
||||||
|
width: number
|
||||||
|
backgroundColor?: string
|
||||||
|
isPressure?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名点位类型
|
||||||
|
* @property x - 点的横坐标
|
||||||
|
* @property y - 点的纵坐标
|
||||||
|
* @property t - 点的时间戳
|
||||||
|
* @property speed - 当前点的绘制速度 (可选)
|
||||||
|
* @property distance - 与上一个点的距离 (可选)
|
||||||
|
* @property lineWidth - 当前点的线宽 (可选,用于笔锋模式)
|
||||||
|
* @property lastX1 - 贝塞尔曲线第一个控制点的x坐标 (可选)
|
||||||
|
* @property lastY1 - 贝塞尔曲线第一个控制点的y坐标 (可选)
|
||||||
|
* @property lastX2 - 贝塞尔曲线第二个控制点的x坐标 (可选)
|
||||||
|
* @property lastY2 - 贝塞尔曲线第二个控制点的y坐标 (可选)
|
||||||
|
* @property isFirstPoint - 是否为线条的第一个点 (可选)
|
||||||
|
*/
|
||||||
|
export interface Point {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
t: number
|
||||||
|
speed?: number
|
||||||
|
distance?: number
|
||||||
|
lineWidth?: number
|
||||||
|
lastX1?: number
|
||||||
|
lastY1?: number
|
||||||
|
lastX2?: number
|
||||||
|
lastY2?: number
|
||||||
|
isFirstPoint?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 签名组件暴露的方法类型
|
||||||
|
* @property init - 初始化签名板
|
||||||
|
* @property clear - 清除签名
|
||||||
|
* @property confirm - 确认签名并生成图片
|
||||||
|
* @property restore - 恢复上一步操作
|
||||||
|
* @property revoke - 撤销上一步操作
|
||||||
|
*/
|
||||||
export type SignatureExpose = {
|
export type SignatureExpose = {
|
||||||
|
/** 初始化签名板
|
||||||
|
* @param forceUpdate - 是否强制更新
|
||||||
|
*/
|
||||||
|
init: (forceUpdate?: boolean) => void
|
||||||
/** 点击清除按钮清除签名 */
|
/** 点击清除按钮清除签名 */
|
||||||
clear: () => void
|
clear: () => void
|
||||||
/** 点击确定按钮 */
|
/** 点击确定按钮 */
|
||||||
confirm: (result: SignatureResult) => void
|
confirm: () => void
|
||||||
/* 点击恢复 */
|
/* 点击恢复 */
|
||||||
restore: () => void
|
restore: () => void
|
||||||
/* 点击撤回 */
|
/* 点击撤回 */
|
||||||
revoke: () => void
|
revoke: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SignatureProps = ExtractPropTypes<typeof signatureProps>
|
||||||
|
|
||||||
|
export type SignatureInstance = ComponentPublicInstance<SignatureExpose, SignatureProps>
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="wd-signature">
|
<view class="wd-signature">
|
||||||
<view class="wd-signature__content" :style="canvasStyle">
|
<view class="wd-signature__content">
|
||||||
<!-- #ifdef MP-WEIXIN -->
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
<canvas
|
<canvas
|
||||||
class="wd-signature__content-canvas"
|
class="wd-signature__content-canvas"
|
||||||
|
:style="canvasStyle"
|
||||||
:width="canvasState.canvasWidth"
|
:width="canvasState.canvasWidth"
|
||||||
:height="canvasState.canvasHeight"
|
:height="canvasState.canvasHeight"
|
||||||
:canvas-id="canvasId"
|
:canvas-id="canvasId"
|
||||||
@ -19,6 +20,7 @@
|
|||||||
<canvas
|
<canvas
|
||||||
class="wd-signature__content-canvas"
|
class="wd-signature__content-canvas"
|
||||||
:canvas-id="canvasId"
|
:canvas-id="canvasId"
|
||||||
|
:style="canvasStyle"
|
||||||
:width="canvasState.canvasWidth"
|
:width="canvasState.canvasWidth"
|
||||||
:height="canvasState.canvasHeight"
|
:height="canvasState.canvasHeight"
|
||||||
:id="canvasId"
|
:id="canvasId"
|
||||||
@ -34,14 +36,18 @@
|
|||||||
name="footer"
|
name="footer"
|
||||||
:clear="clear"
|
:clear="clear"
|
||||||
:confirm="confirmSignature"
|
:confirm="confirmSignature"
|
||||||
:currentStep="currentStep"
|
:current-step="currentStep"
|
||||||
:revoke="revoke"
|
:revoke="revoke"
|
||||||
:restore="restore"
|
:restore="restore"
|
||||||
:historyList="historyList"
|
:can-undo="lines.length > 0"
|
||||||
|
:can-redo="redoLines.length > 0"
|
||||||
|
:history-list="lines"
|
||||||
>
|
>
|
||||||
<block v-if="history">
|
<block v-if="enableHistory">
|
||||||
<wd-button size="small" plain @click="revoke" :disabled="currentStep <= 0">{{ revokeText || translate('revokeText') }}</wd-button>
|
<wd-button size="small" plain @click="revoke" :disabled="lines.length <= 0">
|
||||||
<wd-button size="small" plain @click="restore" :disabled="!(currentStep < historyList.length)">
|
{{ revokeText || translate('revokeText') }}
|
||||||
|
</wd-button>
|
||||||
|
<wd-button size="small" plain @click="restore" :disabled="redoLines.length <= 0">
|
||||||
{{ restoreText || translate('restoreText') }}
|
{{ restoreText || translate('restoreText') }}
|
||||||
</wd-button>
|
</wd-button>
|
||||||
</block>
|
</block>
|
||||||
@ -64,7 +70,7 @@ export default {
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, getCurrentInstance, onBeforeMount, onMounted, reactive, ref, watch, type CSSProperties } from 'vue'
|
import { computed, getCurrentInstance, onBeforeMount, onMounted, reactive, ref, watch, type CSSProperties } from 'vue'
|
||||||
import { addUnit, getRect, isDef, objToStyle, uuid } from '../common/util'
|
import { addUnit, getRect, isDef, objToStyle, uuid } from '../common/util'
|
||||||
import { signatureProps, type SignatureExpose, type SignatureResult } from './types'
|
import { signatureProps, type SignatureExpose, type SignatureResult, type Point, type Line } from './types'
|
||||||
import { useTranslate } from '../composables/useTranslate'
|
import { useTranslate } from '../composables/useTranslate'
|
||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN
|
||||||
import { canvas2dAdapter } from '../common/canvasHelper'
|
import { canvas2dAdapter } from '../common/canvasHelper'
|
||||||
@ -78,9 +84,6 @@ const canvasId = ref<string>(`signature${uuid()}`) // canvas 组件的唯一标
|
|||||||
let canvas: null = null //canvas对象 微信小程序生成图片必须传入
|
let canvas: null = null //canvas对象 微信小程序生成图片必须传入
|
||||||
const drawing = ref<boolean>(false) // 是否正在绘制
|
const drawing = ref<boolean>(false) // 是否正在绘制
|
||||||
const pixelRatio = ref<number>(1) // 像素比
|
const pixelRatio = ref<number>(1) // 像素比
|
||||||
const historyList = ref<Array<ImageData>>([]) //历史记录
|
|
||||||
const currentStep = ref(0) // 当前步骤
|
|
||||||
const maxHistoryLength = ref<number>(100) // 历史记录的最大长度
|
|
||||||
|
|
||||||
const canvasState = reactive({
|
const canvasState = reactive({
|
||||||
canvasWidth: 0,
|
canvasWidth: 0,
|
||||||
@ -116,37 +119,99 @@ const canvasStyle = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const disableScroll = computed(() => props.disableScroll)
|
const disableScroll = computed(() => props.disableScroll)
|
||||||
const history = computed(() => props.history)
|
const enableHistory = computed(() => props.enableHistory)
|
||||||
|
|
||||||
|
const lines = ref<Line[]>([]) // 保存所有线条
|
||||||
|
const redoLines = ref<Line[]>([]) // 保存撤销的线条
|
||||||
|
const currentLine = ref<Line>() // 当前正在绘制的线
|
||||||
|
const currentStep = ref(0) // 当前步骤
|
||||||
|
|
||||||
|
// 添加计算笔画宽度的方法
|
||||||
|
function calculateLineWidth(speed: number): number {
|
||||||
|
if (!props.pressure) return props.lineWidth
|
||||||
|
|
||||||
|
const minSpeed = props.minSpeed || 1.5
|
||||||
|
const limitedSpeed = Math.min(minSpeed * 10, Math.max(minSpeed, speed))
|
||||||
|
const addWidth = ((props.maxWidth - props.minWidth) * (limitedSpeed - minSpeed)) / minSpeed
|
||||||
|
const lineWidth = Math.max(props.maxWidth - addWidth, props.minWidth)
|
||||||
|
return Math.min(lineWidth, props.maxWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 获取默认笔画宽度 */
|
||||||
|
const getDefaultLineWidth = () => {
|
||||||
|
if (props.pressure) {
|
||||||
|
// 在压感模式下,使用最大和最小宽度的平均值作为默认值
|
||||||
|
return (props.maxWidth + props.minWidth) / 2
|
||||||
|
}
|
||||||
|
return props.lineWidth
|
||||||
|
}
|
||||||
|
|
||||||
/* 开始画线 */
|
/* 开始画线 */
|
||||||
const startDrawing = (e: TouchEvent) => {
|
const startDrawing = (e: any) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
drawing.value = true
|
drawing.value = true
|
||||||
setLine()
|
setLine()
|
||||||
emit('start', e)
|
emit('start', e)
|
||||||
draw(e)
|
|
||||||
// 如果当前步骤不是最后一步,则替换历史记录
|
// 创建新线条,同时保存当前的所有绘制参数
|
||||||
if (history.value) {
|
const { x, y } = e.touches[0]
|
||||||
if (currentStep.value < historyList.value.length) {
|
currentLine.value = {
|
||||||
historyList.value = historyList.value.slice(0, currentStep.value)
|
points: [
|
||||||
}
|
{
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
t: Date.now() // 使用 t 替换 width
|
||||||
|
}
|
||||||
|
],
|
||||||
|
color: props.penColor,
|
||||||
|
width: getDefaultLineWidth(),
|
||||||
|
backgroundColor: props.backgroundColor,
|
||||||
|
isPressure: props.pressure // 添加笔锋模式标记
|
||||||
}
|
}
|
||||||
// 更新当前步骤
|
|
||||||
currentStep.value = historyList.value.length
|
// 清空重做记录
|
||||||
|
redoLines.value = []
|
||||||
|
draw(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 结束画线 */
|
/* 结束画线 */
|
||||||
const stopDrawing = (e: TouchEvent) => {
|
const stopDrawing = (e: TouchEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
drawing.value = false
|
drawing.value = false
|
||||||
|
if (currentLine.value) {
|
||||||
|
// 保存完整的线条信息,包括所有点的参数
|
||||||
|
lines.value.push({
|
||||||
|
...currentLine.value,
|
||||||
|
points: currentLine.value.points.map((point) => ({
|
||||||
|
...point,
|
||||||
|
t: point.t,
|
||||||
|
speed: point.speed,
|
||||||
|
distance: point.distance,
|
||||||
|
lineWidth: point.lineWidth,
|
||||||
|
lastX1: point.lastX1,
|
||||||
|
lastY1: point.lastY1,
|
||||||
|
lastX2: point.lastX2,
|
||||||
|
lastY2: point.lastY2,
|
||||||
|
isFirstPoint: point.isFirstPoint
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
currentStep.value = lines.value.length
|
||||||
|
}
|
||||||
|
currentLine.value = undefined
|
||||||
const { ctx } = canvasState
|
const { ctx } = canvasState
|
||||||
if (ctx) ctx.beginPath()
|
if (ctx) ctx.beginPath()
|
||||||
pushHistoryList()
|
|
||||||
emit('end', e)
|
emit('end', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化 canvas
|
/**
|
||||||
const initCanvas = () => {
|
* 初始化 canvas
|
||||||
|
* @param forceUpdate 是否强制更新
|
||||||
|
*/
|
||||||
|
const initCanvas = (forceUpdate: boolean = false) => {
|
||||||
|
// 如果不是强制更新,且已经初始化过 canvas,则不再重复初始化
|
||||||
|
if (!forceUpdate && canvasState.canvasHeight && canvasState.canvasWidth) {
|
||||||
|
return
|
||||||
|
}
|
||||||
getContext().then(() => {
|
getContext().then(() => {
|
||||||
const { ctx } = canvasState
|
const { ctx } = canvasState
|
||||||
if (ctx && isDef(props.backgroundColor)) {
|
if (ctx && isDef(props.backgroundColor)) {
|
||||||
@ -159,7 +224,9 @@ const initCanvas = () => {
|
|||||||
|
|
||||||
// 清空 canvas
|
// 清空 canvas
|
||||||
const clear = () => {
|
const clear = () => {
|
||||||
clearHistoryList()
|
lines.value = []
|
||||||
|
redoLines.value = []
|
||||||
|
currentStep.value = 0
|
||||||
clearCanvas()
|
clearCanvas()
|
||||||
emit('clear')
|
emit('clear')
|
||||||
}
|
}
|
||||||
@ -176,39 +243,217 @@ const draw = (e: any) => {
|
|||||||
|
|
||||||
if (!drawing.value || props.disabled || !ctx) return
|
if (!drawing.value || props.disabled || !ctx) return
|
||||||
const { x, y } = e.touches[0]
|
const { x, y } = e.touches[0]
|
||||||
ctx.lineTo(x, y)
|
|
||||||
ctx.stroke()
|
const point: Point = {
|
||||||
ctx.draw(true) //是否记住上一次画线
|
x,
|
||||||
ctx.moveTo(x, y)
|
y,
|
||||||
emit('signing', e)
|
t: Date.now()
|
||||||
}
|
}
|
||||||
/* 点击上一步 */
|
|
||||||
const revoke = () => {
|
if (currentLine.value) {
|
||||||
if (history.value) {
|
const points = currentLine.value.points
|
||||||
if (isDef(props.step)) {
|
const prePoint = points[points.length - 1]
|
||||||
currentStep.value = Math.max(currentStep.value - props.step, 0)
|
|
||||||
if (currentStep.value > 0) {
|
if (prePoint.t === point.t || (prePoint.x === x && prePoint.y === y)) {
|
||||||
clearCanvas()
|
return
|
||||||
putCanvasImageData(props.step)
|
}
|
||||||
} else {
|
|
||||||
clearCanvas()
|
// 计算点的速度和距离
|
||||||
currentStep.value = 0
|
point.distance = Math.sqrt(Math.pow(point.x - prePoint.x, 2) + Math.pow(point.y - prePoint.y, 2))
|
||||||
|
point.speed = point.distance / (point.t - prePoint.t || 0.1)
|
||||||
|
|
||||||
|
if (props.pressure) {
|
||||||
|
point.lineWidth = calculateLineWidth(point.speed)
|
||||||
|
// 处理线宽变化率限制
|
||||||
|
if (points.length >= 2) {
|
||||||
|
const prePoint2 = points[points.length - 2]
|
||||||
|
if (prePoint2.lineWidth && prePoint.lineWidth) {
|
||||||
|
const rate = (point.lineWidth - prePoint.lineWidth) / prePoint.lineWidth
|
||||||
|
const maxRate = 0.2 // 最大变化率20%
|
||||||
|
if (Math.abs(rate) > maxRate) {
|
||||||
|
const per = rate > 0 ? maxRate : -maxRate
|
||||||
|
point.lineWidth = prePoint.lineWidth * (1 + per)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
points.push(point)
|
||||||
|
|
||||||
|
// 非笔锋模式直接使用线段连接
|
||||||
|
if (!props.pressure) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(prePoint.x, prePoint.y)
|
||||||
|
ctx.lineTo(point.x, point.y)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.draw(true)
|
||||||
|
} else if (points.length >= 2) {
|
||||||
|
// 笔锋模式使用贝塞尔曲线
|
||||||
|
drawSmoothLine(prePoint, point)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
/* 点击下一步 */
|
|
||||||
const restore = () => {
|
|
||||||
if (history.value) {
|
|
||||||
if (isDef(props.step)) {
|
|
||||||
/* 是否可以点击下一步 */
|
|
||||||
|
|
||||||
if (currentStep.value <= historyList.value.length - props.step) {
|
emit('signing', e)
|
||||||
currentStep.value = currentStep.value + props.step
|
}
|
||||||
clearCanvas()
|
|
||||||
putCanvasImageData(props.step)
|
/* 重绘整个画布 */
|
||||||
}
|
const redrawCanvas = () => {
|
||||||
|
const { ctx } = canvasState
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
// 清除画布并设置背景
|
||||||
|
if (isDef(props.backgroundColor)) {
|
||||||
|
ctx.setFillStyle(props.backgroundColor)
|
||||||
|
ctx.fillRect(0, 0, canvasState.canvasWidth, canvasState.canvasHeight)
|
||||||
|
} else {
|
||||||
|
ctx.clearRect(0, 0, canvasState.canvasWidth, canvasState.canvasHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有线条,只需要清空画布
|
||||||
|
if (lines.value.length === 0) {
|
||||||
|
ctx.draw()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集所有绘制操作,最后一次性 draw
|
||||||
|
lines.value.forEach((line) => {
|
||||||
|
if (!line.points.length) return
|
||||||
|
|
||||||
|
ctx.setStrokeStyle(line.color)
|
||||||
|
ctx.setLineJoin('round')
|
||||||
|
ctx.setLineCap('round')
|
||||||
|
|
||||||
|
if (line.isPressure && props.pressure) {
|
||||||
|
// 笔锋模式的重绘
|
||||||
|
line.points.forEach((point, index) => {
|
||||||
|
if (index === 0) return
|
||||||
|
const prePoint = line.points[index - 1]
|
||||||
|
const dis_x = point.x - prePoint.x
|
||||||
|
const dis_y = point.y - prePoint.y
|
||||||
|
const distance = Math.sqrt(dis_x * dis_x + dis_y * dis_y)
|
||||||
|
|
||||||
|
if (distance <= 2) {
|
||||||
|
point.lastX1 = point.lastX2 = prePoint.x + dis_x * 0.5
|
||||||
|
point.lastY1 = point.lastY2 = prePoint.y + dis_y * 0.5
|
||||||
|
} else {
|
||||||
|
const speed = point.speed || 0
|
||||||
|
const minSpeed = props.minSpeed || 1.5
|
||||||
|
const speedFactor = Math.max(0.1, Math.min(0.9, speed / (minSpeed * 10)))
|
||||||
|
|
||||||
|
point.lastX1 = prePoint.x + dis_x * (0.2 + speedFactor * 0.3)
|
||||||
|
point.lastY1 = prePoint.y + dis_y * (0.2 + speedFactor * 0.3)
|
||||||
|
point.lastX2 = prePoint.x + dis_x * (0.8 - speedFactor * 0.3)
|
||||||
|
point.lastY2 = prePoint.y + dis_y * (0.8 - speedFactor * 0.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineWidth = point.lineWidth || line.width
|
||||||
|
if (typeof prePoint.lastX1 === 'number') {
|
||||||
|
ctx.setLineWidth(lineWidth)
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(prePoint.lastX2!, prePoint.lastY2!)
|
||||||
|
ctx.quadraticCurveTo(prePoint.x, prePoint.y, point.lastX1, point.lastY1)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
if (!prePoint.isFirstPoint) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(prePoint.lastX1!, prePoint.lastY1!)
|
||||||
|
ctx.quadraticCurveTo(prePoint.x, prePoint.y, prePoint.lastX2!, prePoint.lastY2!)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
point.isFirstPoint = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 非笔锋模式的重绘
|
||||||
|
ctx.setLineWidth(line.width)
|
||||||
|
line.points.forEach((point, index) => {
|
||||||
|
if (index === 0) return
|
||||||
|
const prePoint = line.points[index - 1]
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(prePoint.x, prePoint.y)
|
||||||
|
ctx.lineTo(point.x, point.y)
|
||||||
|
ctx.stroke()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 所有线条绘制完成后,一次性更新画布
|
||||||
|
ctx.draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改撤销功能
|
||||||
|
const revoke = () => {
|
||||||
|
if (!lines.value.length) return
|
||||||
|
const step = Math.min(props.step, lines.value.length)
|
||||||
|
const removedLines = lines.value.splice(lines.value.length - step)
|
||||||
|
redoLines.value.push(...removedLines)
|
||||||
|
currentStep.value = Math.max(0, currentStep.value - step)
|
||||||
|
redrawCanvas()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改恢复功能
|
||||||
|
const restore = () => {
|
||||||
|
if (!redoLines.value.length) return
|
||||||
|
const step = Math.min(props.step, redoLines.value.length)
|
||||||
|
const restoredLines = redoLines.value.splice(redoLines.value.length - step)
|
||||||
|
lines.value.push(...restoredLines)
|
||||||
|
currentStep.value = Math.min(lines.value.length, currentStep.value + step)
|
||||||
|
redrawCanvas()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加平滑线条绘制方法
|
||||||
|
function drawSmoothLine(prePoint: Point, point: Point) {
|
||||||
|
const { ctx } = canvasState
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
// 计算两点间距离
|
||||||
|
const dis_x = point.x - prePoint.x
|
||||||
|
const dis_y = point.y - prePoint.y
|
||||||
|
const distance = Math.sqrt(dis_x * dis_x + dis_y * dis_y)
|
||||||
|
|
||||||
|
if (distance <= 2) {
|
||||||
|
// 对于非常近的点,直接使用中点
|
||||||
|
point.lastX1 = point.lastX2 = prePoint.x + dis_x * 0.5
|
||||||
|
point.lastY1 = point.lastY2 = prePoint.y + dis_y * 0.5
|
||||||
|
} else {
|
||||||
|
// 根据点的速度计算控制点的偏移程度
|
||||||
|
const speed = point.speed || 0
|
||||||
|
const minSpeed = props.minSpeed || 1.5
|
||||||
|
const speedFactor = Math.max(0.1, Math.min(0.9, speed / (minSpeed * 10)))
|
||||||
|
|
||||||
|
// 计算控制点
|
||||||
|
point.lastX1 = prePoint.x + dis_x * (0.2 + speedFactor * 0.3)
|
||||||
|
point.lastY1 = prePoint.y + dis_y * (0.2 + speedFactor * 0.3)
|
||||||
|
point.lastX2 = prePoint.x + dis_x * (0.8 - speedFactor * 0.3)
|
||||||
|
point.lastY2 = prePoint.y + dis_y * (0.8 - speedFactor * 0.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算线宽
|
||||||
|
const lineWidth = point.lineWidth || props.lineWidth
|
||||||
|
|
||||||
|
// 绘制贝塞尔曲线
|
||||||
|
if (typeof prePoint.lastX1 === 'number') {
|
||||||
|
// 设置线宽
|
||||||
|
ctx.setLineWidth(lineWidth)
|
||||||
|
// 绘制第一段曲线
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(prePoint.lastX2!, prePoint.lastY2!)
|
||||||
|
ctx.quadraticCurveTo(prePoint.x, prePoint.y, point.lastX1, point.lastY1)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
if (!prePoint.isFirstPoint) {
|
||||||
|
// 绘制连接段曲线
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(prePoint.lastX1!, prePoint.lastY1!)
|
||||||
|
ctx.quadraticCurveTo(prePoint.x, prePoint.y, prePoint.lastX2!, prePoint.lastY2!)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量更新绘制内容
|
||||||
|
ctx.draw(true)
|
||||||
|
} else {
|
||||||
|
point.isFirstPoint = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,7 +490,7 @@ function getContext() {
|
|||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN
|
||||||
|
|
||||||
getRect(`#${canvasId.value}`, false, proxy, true).then((canvasRect: any) => {
|
getRect(`#${canvasId.value}`, false, proxy, true).then((canvasRect: any) => {
|
||||||
if (canvasRect && canvasRect.node) {
|
if (canvasRect && canvasRect.node && canvasRect.width && canvasRect.height) {
|
||||||
const canvasInstance = canvasRect.node
|
const canvasInstance = canvasRect.node
|
||||||
canvasState.ctx = canvas2dAdapter(canvasInstance.getContext('2d') as CanvasRenderingContext2D)
|
canvasState.ctx = canvas2dAdapter(canvasInstance.getContext('2d') as CanvasRenderingContext2D)
|
||||||
canvasInstance.width = canvasRect.width * pixelRatio.value
|
canvasInstance.width = canvasRect.width * pixelRatio.value
|
||||||
@ -272,7 +517,7 @@ function setcanvasState(width: number, height: number) {
|
|||||||
function setLine() {
|
function setLine() {
|
||||||
const { ctx } = canvasState
|
const { ctx } = canvasState
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
ctx.setLineWidth(props.lineWidth)
|
ctx.setLineWidth(getDefaultLineWidth()) // 使用新的默认宽度
|
||||||
ctx.setStrokeStyle(props.penColor)
|
ctx.setStrokeStyle(props.penColor)
|
||||||
ctx.setLineJoin('round')
|
ctx.setLineJoin('round')
|
||||||
ctx.setLineCap('round')
|
ctx.setLineCap('round')
|
||||||
@ -320,80 +565,6 @@ function canvasToImage() {
|
|||||||
proxy
|
proxy
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/* canvas获取每一步的图片 */
|
|
||||||
function getCanvasImageData(): Promise<ImageData> {
|
|
||||||
const { canvasWidth, canvasHeight, ctx } = canvasState
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// #ifdef MP-WEIXIN
|
|
||||||
try {
|
|
||||||
// 获取图像数据
|
|
||||||
if (ctx) {
|
|
||||||
const imageData = (ctx as unknown as CanvasRenderingContext2D).getImageData(0, 0, canvasWidth, canvasHeight)
|
|
||||||
|
|
||||||
resolve(imageData)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取 canvas 像素数据失败:', error)
|
|
||||||
reject(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// #endif
|
|
||||||
|
|
||||||
// #ifndef MP-WEIXIN
|
|
||||||
uni.canvasGetImageData({
|
|
||||||
canvasId: canvasId.value,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: canvasWidth,
|
|
||||||
height: canvasHeight,
|
|
||||||
success: (res: any) => {
|
|
||||||
resolve(res)
|
|
||||||
},
|
|
||||||
fail: (err) => {
|
|
||||||
console.error('获取 canvas 像素数据失败:')
|
|
||||||
reject(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// #endif
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function putCanvasImageData(step: number = 1) {
|
|
||||||
const { canvasWidth, canvasHeight, ctx } = canvasState
|
|
||||||
const imagedata = historyList.value[currentStep.value - step]
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// #ifdef MP-WEIXIN
|
|
||||||
try {
|
|
||||||
if (ctx) {
|
|
||||||
;(ctx as unknown as CanvasRenderingContext2D).putImageData(imagedata, 0, 0)
|
|
||||||
resolve(true)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取 canvas 像素数据失败:', error)
|
|
||||||
reject(error)
|
|
||||||
}
|
|
||||||
// #endif
|
|
||||||
|
|
||||||
// #ifndef MP-WEIXIN
|
|
||||||
uni.canvasPutImageData({
|
|
||||||
canvasId: canvasId.value,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: canvasWidth,
|
|
||||||
height: canvasHeight,
|
|
||||||
data: imagedata.data,
|
|
||||||
success: (res) => {
|
|
||||||
resolve(res)
|
|
||||||
},
|
|
||||||
fail: (err) => {
|
|
||||||
console.error('获取 canvas 像素数据失败:', err)
|
|
||||||
reject(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// #endif
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearCanvas() {
|
function clearCanvas() {
|
||||||
const { canvasWidth, canvasHeight, ctx } = canvasState
|
const { canvasWidth, canvasHeight, ctx } = canvasState
|
||||||
@ -406,37 +577,9 @@ function clearCanvas() {
|
|||||||
ctx.draw()
|
ctx.draw()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* pushHistoryList */
|
|
||||||
function pushHistoryList() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
console.log(history.value)
|
|
||||||
if (history.value) {
|
|
||||||
getCanvasImageData()
|
|
||||||
.then((imageData) => {
|
|
||||||
historyList.value.push(imageData)
|
|
||||||
currentStep.value++
|
|
||||||
// 如果历史记录超过最大长度,则删除最早的记录
|
|
||||||
if (historyList.value.length > maxHistoryLength.value) {
|
|
||||||
historyList.value.shift()
|
|
||||||
currentStep.value--
|
|
||||||
}
|
|
||||||
resolve(true)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearHistoryList() {
|
|
||||||
if (history.value) {
|
|
||||||
historyList.value = []
|
|
||||||
currentStep.value = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose<SignatureExpose>({
|
defineExpose<SignatureExpose>({
|
||||||
|
init: initCanvas,
|
||||||
clear,
|
clear,
|
||||||
confirm: confirmSignature,
|
confirm: confirmSignature,
|
||||||
restore,
|
restore,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user