From 456b21846e549dcfed9524d1c8325c44ec6afafe Mon Sep 17 00:00:00 2001
From: YFeng <499756901@qq.com>
Date: Tue, 27 Feb 2024 18:34:38 +0800
Subject: [PATCH] 实现小程序中签名

---
 forms/userSignature/cUserSignature.scss  |    3 
 forms/userSignature/CUserSignature.vue   |   25 ++
 forms/userSignature/CSignatureLayer.vue  |  432 +++++++++++++++++++++++++++++++++++++++++++
 forms/userSignature/cSignatureLayer.scss |   87 ++++++++
 4 files changed, 542 insertions(+), 5 deletions(-)

diff --git a/forms/userSignature/CSignatureLayer.vue b/forms/userSignature/CSignatureLayer.vue
new file mode 100644
index 0000000..23f6766
--- /dev/null
+++ b/forms/userSignature/CSignatureLayer.vue
@@ -0,0 +1,432 @@
+/**
+ * CSignatureLayer - 签名层
+ * @author Tevin
+ */
+
+<template>
+    <AtFloatLayout
+        class="c-signature-layer"
+        title="手写板 / 请书写签名"
+        :isOpened="layerOpened"
+        :onClose="evt => null"
+    >
+        <view class="c-signature-layer-draw">
+            <view class="size-box-top"></view>
+            <view class="size-box-bottom"></view>
+            <canvas
+                class="drawing"
+                ref="drawing"
+                :canvasId="cavId"
+                :width="cavWidth"
+                :height="cavHeight"
+                :disableScroll="true"
+                @touchstart="evt => handleWriteStart(evt)"
+                @touchmove="evt => handleWriteMove(evt)"
+                @touchend="evt => handleWriteEnd(evt)"
+            ></canvas>
+        </view>
+        <view class="c-signature-layer-btns">
+            <AtButton
+                class="btn-warning"
+                type="primary"
+                size="large"
+                :onClick="evt => handleRestDraw()"
+            >重写</AtButton>
+            <AtButton
+                type="primary"
+                size="large"
+                :onClick="evt => handleSaveDraw()"
+            >完成</AtButton>
+        </view>
+    </AtFloatLayout>
+</template>
+
+<script>
+import Taro from '@tarojs/taro';
+import { $ } from '@tarojs/extend';
+import { AtFloatLayout, AtButton } from 'taro-ui-vue';
+import './cSignatureLayer.scss';
+import { Tools } from '@components/common/Tools';
+
+export default {
+    name: 'CSignatureLayer',
+    components: {
+        AtFloatLayout,
+        AtButton,
+    },
+    props: {},
+    data() {
+        return {
+            cavId: 'signCanvas-' + Date.now() + '-' + parseInt(Math.random() * 10000),
+            layerOpened: false,
+            cavWidth: 0,
+            cavHeight: 0,
+            curPoint: {},
+            lastPoint: {},
+            curLine: [],
+            // 第一次触发
+            firstTouch: true,
+            // 所有笔迹
+            chirography: [],
+            // 初始画圆的半径
+            radius: 1,
+        };
+    },
+    methods: {
+        $onDraw(callback) {
+            this.layerOpened = true;
+            this._callback = callback;
+            this.handleRestDraw();
+        },
+        handleRestDraw() {
+            this.firstTouch = true;
+            this.curLine = [];
+            this.chirography = [];
+            this.canvasContext.clearRect(0, 0, this.cavWidth, this.cavHeight);
+            this.canvasContext.draw();
+        },
+        handleWriteStart(evt) {
+            if (evt.type != 'touchstart') {
+                return false;
+            }
+            this.canvasContext.setFillStyle('#1A1A1A');
+            const point = {
+                x: evt.touches[0].x,
+                y: evt.touches[0].y,
+            };
+            this.curLine.unshift({
+                time: Date.now(),
+                dis: 0,
+                x: point.x,
+                y: point.y,
+            });
+            if (this.firstTouch) {
+                this.cutArea = {
+                    top: point.y,
+                    right: point.x,
+                    bottom: point.y,
+                    left: point.x,
+                };
+                this.firstTouch = false;
+            }
+            this._pointToLine(this.curLine);
+        },
+        handleWriteMove(evt) {
+            if (evt.type != 'touchmove') {
+                return false;
+            }
+            if (evt.cancelable) {
+                // 判断默认行为是否已经被禁用
+                if (!evt.defaultPrevented) {
+                    evt.preventDefault();
+                }
+            }
+            const point = {
+                x: evt.touches[0].x,
+                y: evt.touches[0].y,
+            };
+            this._updateCutArea(point);
+            this.lastPoint = this.curPoint;
+            this.curPoint = point;
+            this.curLine.unshift({
+                time: Date.now(),
+                dis: this._distance(point, this.lastPoint),
+                x: point.x,
+                y: point.y,
+            });
+            this._pointToLine(this.curLine);
+        },
+        handleWriteEnd(evt) {
+            if (evt.type != 'touchend') {
+                return 0;
+            }
+            const point = {
+                x: evt.changedTouches[0].x,
+                y: evt.changedTouches[0].y,
+            };
+            this.lastPoint = this.curPoint;
+            this.curPoint = point;
+            this.curLine.unshift({
+                time: Date.now(),
+                dis: this._distance(point, this.lastPoint),
+                x: point.x,
+                y: point.y,
+            });
+            this._pointToLine(this.curLine);
+            this.chirography.unshift(this.curLine);
+            this.curLine = [];
+        },
+        // 更新裁剪区域
+        _updateCutArea(point) {
+            if (point.y < this.cutArea.top) {
+                this.cutArea.top = point.y;
+            }
+            if (point.y < 0) {
+                this.cutArea.top = 0;
+            }
+            if (point.x > this.cutArea.right) {
+                this.cutArea.right = point.x;
+            }
+            if (this.cavWidth - point.x <= 0) {
+                this.cutArea.right = this.cavWidth;
+            }
+            if (point.y > this.cutArea.bottom) {
+                this.cutArea.bottom = point.y;
+            }
+            if (this.cavHeight - point.y <= 0) {
+                this.cutArea.bottom = this.cavHeight;
+            }
+            if (point.x < this.cutArea.left) {
+                this.cutArea.left = point.x;
+            }
+            if (point.x < 0) {
+                this.cutArea.left = 0;
+            }
+        },
+        // 求两点之间距离
+        _distance(a, b) {
+            let x = b.x - a.x;
+            let y = b.y - a.y;
+            return Math.sqrt(x * x + y * y);
+        },
+        // 绘制笔迹
+        _pointToLine(line) {
+            this._calcBethelLine(line);
+        },
+        // 计算插值的方式
+        _calcBethelLine(line) {
+            if (line.length <= 1) {
+                line[0].r = this.radius;
+                return;
+            }
+            // 笔迹倍数
+            const lineSize = 1.5;
+            // 最小笔画半径
+            const lineMin = 0.5;
+            // 最大笔画半径
+            const lineMax = 4;
+            // 默认压力
+            const pressure = 1;
+            // 顺滑度,用60的距离来计算速度
+            const smoothness = 60;
+            // 开始计算
+            let x0,
+                x1,
+                x2,
+                y0,
+                y1,
+                y2,
+                r0,
+                r1,
+                r2,
+                len,
+                lastRadius,
+                dis = 0,
+                time = 0,
+                curveValue = 0.5;
+            if (line.length <= 2) {
+                x0 = line[1].x;
+                y0 = line[1].y;
+                x2 = line[1].x + (line[0].x - line[1].x) * curveValue;
+                y2 = line[1].y + (line[0].y - line[1].y) * curveValue;
+                //x2 = line[1].x;
+                //y2 = line[1].y;
+                x1 = x0 + (x2 - x0) * curveValue;
+                y1 = y0 + (y2 - y0) * curveValue;
+            } else {
+                x0 = line[2].x + (line[1].x - line[2].x) * curveValue;
+                y0 = line[2].y + (line[1].y - line[2].y) * curveValue;
+                x1 = line[1].x;
+                y1 = line[1].y;
+                x2 = x1 + (line[0].x - x1) * curveValue;
+                y2 = y1 + (line[0].y - y1) * curveValue;
+            }
+            // 从计算公式看,三个点分别是(x0,y0),(x1,y1),(x2,y2) ;(x1,y1)这个是控制点,控制点不会落在曲线上;实际上,这个点还会手写获取的实际点,却落在曲线上
+            len = this._distance({ x: x2, y: y2 }, { x: x0, y: y0 });
+            lastRadius = this.radius;
+            for (let n = 0; n < line.length - 1; n++) {
+                dis += line[n].dis;
+                time += line[n].time - line[n + 1].time;
+                if (dis > smoothness) {
+                    break;
+                }
+            }
+            this.radius = Math.min((time / len) * pressure + lineMin, lineMax) * lineSize;
+            line[0].r = this.radius;
+            //计算笔迹半径;
+            if (line.length <= 2) {
+                r0 = (lastRadius + this.radius) / 2;
+                r1 = r0;
+                r2 = r1;
+                //return;
+            } else {
+                r0 = (line[2].r + line[1].r) / 2;
+                r1 = line[1].r;
+                r2 = (line[1].r + line[0].r) / 2;
+            }
+            let n = 5;
+            let point = [];
+            for (let i = 0; i < n; i++) {
+                let t = i / (n - 1);
+                let x = (1 - t) * (1 - t) * x0 + 2 * t * (1 - t) * x1 + t * t * x2;
+                let y = (1 - t) * (1 - t) * y0 + 2 * t * (1 - t) * y1 + t * t * y2;
+                let r = lastRadius + ((this.radius - lastRadius) / n) * i;
+                point.push({ x: x, y: y, r: r });
+                if (point.length == 3) {
+                    let a = this._ctaCalc(
+                        point[0].x,
+                        point[0].y,
+                        point[0].r,
+                        point[1].x,
+                        point[1].y,
+                        point[1].r,
+                        point[2].x,
+                        point[2].y,
+                        point[2].r
+                    );
+                    this._bethelDraw(a, true);
+                    point = [{ x: x, y: y, r: r }];
+                }
+            }
+            this.curLine = line;
+        },
+        _ctaCalc(x0, y0, r0, x1, y1, r1, x2, y2, r2) {
+            let a = [],
+                vx01,
+                vy01,
+                norm,
+                n_x0,
+                n_y0,
+                vx21,
+                vy21,
+                n_x2,
+                n_y2;
+            vx01 = x1 - x0;
+            vy01 = y1 - y0;
+            norm = Math.sqrt(vx01 * vx01 + vy01 * vy01 + 0.0001) * 2;
+            vx01 = (vx01 / norm) * r0;
+            vy01 = (vy01 / norm) * r0;
+            n_x0 = vy01;
+            n_y0 = -vx01;
+            vx21 = x1 - x2;
+            vy21 = y1 - y2;
+            norm = Math.sqrt(vx21 * vx21 + vy21 * vy21 + 0.0001) * 2;
+            vx21 = (vx21 / norm) * r2;
+            vy21 = (vy21 / norm) * r2;
+            n_x2 = -vy21;
+            n_y2 = vx21;
+            a.push({ mx: x0 + n_x0, my: y0 + n_y0, color: '#1A1A1A' });
+            a.push({
+                c1x: x1 + n_x0,
+                c1y: y1 + n_y0,
+                c2x: x1 + n_x2,
+                c2y: y1 + n_y2,
+                ex: x2 + n_x2,
+                ey: y2 + n_y2,
+            });
+            a.push({
+                c1x: x2 + n_x2 - vx21,
+                c1y: y2 + n_y2 - vy21,
+                c2x: x2 - n_x2 - vx21,
+                c2y: y2 - n_y2 - vy21,
+                ex: x2 - n_x2,
+                ey: y2 - n_y2,
+            });
+            a.push({
+                c1x: x1 - n_x2,
+                c1y: y1 - n_y2,
+                c2x: x1 - n_x0,
+                c2y: y1 - n_y0,
+                ex: x0 - n_x0,
+                ey: y0 - n_y0,
+            });
+            a.push({
+                c1x: x0 - n_x0 - vx01,
+                c1y: y0 - n_y0 - vy01,
+                c2x: x0 + n_x0 - vx01,
+                c2y: y0 + n_y0 - vy01,
+                ex: x0 + n_x0,
+                ey: y0 + n_y0,
+            });
+            a[0].mx = a[0].mx.toFixed(1);
+            a[0].mx = parseFloat(a[0].mx);
+            a[0].my = a[0].my.toFixed(1);
+            a[0].my = parseFloat(a[0].my);
+            for (let i = 1; i < a.length; i++) {
+                a[i].c1x = a[i].c1x.toFixed(1);
+                a[i].c1x = parseFloat(a[i].c1x);
+                a[i].c1y = a[i].c1y.toFixed(1);
+                a[i].c1y = parseFloat(a[i].c1y);
+                a[i].c2x = a[i].c2x.toFixed(1);
+                a[i].c2x = parseFloat(a[i].c2x);
+                a[i].c2y = a[i].c2y.toFixed(1);
+                a[i].c2y = parseFloat(a[i].c2y);
+                a[i].ex = a[i].ex.toFixed(1);
+                a[i].ex = parseFloat(a[i].ex);
+                a[i].ey = a[i].ey.toFixed(1);
+                a[i].ey = parseFloat(a[i].ey);
+            }
+            return a;
+        },
+        _bethelDraw(point, isFill) {
+            const ctx = this.canvasContext;
+            ctx.beginPath();
+            ctx.moveTo(point[0].mx, point[0].my);
+            for (let i = 1; i < point.length; i++) {
+                ctx.bezierCurveTo(
+                    point[i].c1x,
+                    point[i].c1y,
+                    point[i].c2x,
+                    point[i].c2y,
+                    point[i].ex,
+                    point[i].ey
+                );
+            }
+            ctx.stroke();
+            if (isFill !== undefined) {
+                // 后绘制的图形会覆盖前面的图形, 绘制时注意先后顺序
+                ctx.fill();
+            }
+            ctx.draw(true);
+        },
+        handleSaveDraw() {
+            if (this.firstTouch) {
+                Tools.toast('请书写签名!');
+            }
+            const delta = 20;
+            const clipArea = { x: 0, y: 0, w: 0, h: 0 };
+            clipArea.x = Math.max(this.cutArea.left - delta, 0);
+            clipArea.y = Math.max(this.cutArea.top - delta, 0);
+            const realRight = Math.min(this.cutArea.right + delta, this.cavWidth);
+            const realBottom = Math.min(this.cutArea.bottom + delta, this.cavHeight);
+            clipArea.w = realRight - clipArea.x;
+            clipArea.h = realBottom - clipArea.y;
+            Taro.canvasToTempFilePath({
+                canvasId: this.cavId,
+                x: clipArea.x,
+                y: clipArea.y,
+                width: clipArea.w,
+                height: clipArea.h,
+                destWidth: clipArea.w,
+                destHeight: clipArea.h,
+                quality: 0.6,
+                fileType: 'jpeg',
+                success: res => {
+                    this._callback(res.tempFilePath);
+                    this.layerOpened = false;
+                },
+            });
+        },
+    },
+    mounted() {
+        this.$nextTick(() => {
+            this.canvasContext = Taro.createCanvasContext(this.cavId, this);
+            const $container = $(this.$refs.drawing).parent();
+            setTimeout(() => {
+                $container.width().then(w => (this.cavWidth = w));
+                $container.height().then(h => (this.cavHeight = h));
+            }, 0);
+        });
+    },
+};
+</script>
\ No newline at end of file
diff --git a/forms/userSignature/CUserSignature.vue b/forms/userSignature/CUserSignature.vue
index f04bf42..8182423 100644
--- a/forms/userSignature/CUserSignature.vue
+++ b/forms/userSignature/CUserSignature.vue
@@ -32,6 +32,10 @@
                 />
             </view>
         </view>
+        <CSignatureLayer
+            v-if="drawSelf"
+            ref="drawLayer"
+        />
     </view>
 </template>
 
@@ -43,6 +47,7 @@
 import { $fetchCommon } from '@fetchers/FCommon';
 import { $bridge } from '@components/common/Bridge';
 import { $hostBoot } from '@components/bases/HostBoot';
+import CSignatureLayer from '@components/forms/userSignature/CSignatureLayer';
 import project from '@project';
 import './cUserSignature.scss';
 
@@ -51,6 +56,7 @@
     components: {
         AtInput,
         AtIcon,
+        CSignatureLayer,
     },
     props: {
         // 表单数据资源(表单组件内部机制专用)
@@ -58,9 +64,7 @@
     },
     data() {
         return {
-            // id: 'CUserSignatureCanvas' + Date.now() + parseInt(Math.random() * 10000),
-            // cavWidth: 1000,
-            // cavHeight: 1600,
+            drawSelf: process.env.TARO_ENV === 'weapp',
         };
     },
     computed: {},
@@ -76,7 +80,18 @@
                     this.itemRes.onChange(url);
                 });
             }
-            // TODO: 普通h5、小程序中,使用 canvas 签名
+            // 小程序中
+            else if (process.env.TARO_ENV === 'weapp') {
+                this.$refs.drawLayer.$onDraw(sign => {
+                    if (sign.indexOf('http') >= 0) {
+                        this.itemRes.onChange(sign);
+                    } else {
+                        const url = this._transBase64ToBlob(sign);
+                        this.itemRes.onChange(url);
+                    }
+                });
+            }
+            // TODO: 普通h5,使用 canvas 签名
         },
         _transBase64ToBlob(base64) {
             const arr = base64.split(',');
@@ -160,7 +175,7 @@
         } else if (process.env.TARO_ENV === 'weapp') {
             $(this.$refs.input.$el)
                 .find('.at-input__container')
-                .append(this.$refs.drawing.$el);
+                .append(this.$refs.drawing);
         }
     },
 };
diff --git a/forms/userSignature/cSignatureLayer.scss b/forms/userSignature/cSignatureLayer.scss
new file mode 100644
index 0000000..3a1d751
--- /dev/null
+++ b/forms/userSignature/cSignatureLayer.scss
@@ -0,0 +1,87 @@
+/**
+ * cUserSignature
+ * @author Tevin
+ */
+
+@import "../../common/sassMixin";
+
+.c-signature-layer {
+    &.at-float-layout {
+        .at-float-layout__container {
+            height: 94%;
+            max-height: 100%;
+        }
+        .layout-body {
+            padding: 0;
+        }
+        .layout-body__content {
+            height: 100%;
+        }
+    }
+    .c-signature-layer-btns {
+        @include position(absolute, n 0 0 n);
+        width: 100%;
+        height: 92px;
+        font-size: 0;
+        .at-button {
+            width: 50%;
+            border-radius: 0;
+        }
+    }
+    .c-signature-layer-draw {
+        position: relative;
+        height: calc(100% - 92px);
+        .size-box-top {
+            @include position(absolute, 3% 3% n n);
+            width: 94%;
+            height: 36px;
+            pointer-events: none;
+            &::before {
+                @include position(absolute, 0 0 n n);
+                width: 36px;
+                height: 36px;
+                border-top: #ccc 1PX solid;
+                border-left: #ccc 1PX solid;
+                border-top-left-radius: 8px;
+                content: " ";
+            }
+            &::after {
+                @include position(absolute, 0 n n 0);
+                width: 36px;
+                height: 36px;
+                border-top: #ccc 1PX solid;
+                border-right: #ccc 1PX solid;
+                border-top-right-radius: 8px;
+                content: " ";
+            }
+        }
+        .size-box-bottom {
+            @include position(absolute, n 3% 3% n);
+            width: 94%;
+            height: 36px;
+            pointer-events: none;
+            &::before {
+                @include position(absolute, n 0 0 n);
+                width: 36px;
+                height: 36px;
+                border-bottom: #ccc 1PX solid;
+                border-left: #ccc 1PX solid;
+                border-bottom-left-radius: 8px;
+                content: " ";
+            }
+            &::after {
+                @include position(absolute, n n 0 0);
+                width: 36px;
+                height: 36px;
+                border-bottom: #ccc 1PX solid;
+                border-right: #ccc 1PX solid;
+                border-bottom-right-radius: 8px;
+                content: " ";
+            }
+        }
+        .drawing {
+            width: 100%;
+            height: 100%;
+        }
+    }
+}
\ No newline at end of file
diff --git a/forms/userSignature/cUserSignature.scss b/forms/userSignature/cUserSignature.scss
index 987bc61..4f1ac34 100644
--- a/forms/userSignature/cUserSignature.scss
+++ b/forms/userSignature/cUserSignature.scss
@@ -13,6 +13,9 @@
         .weui-input {
             display: none;
         }
+        .at-input__input {
+            display: none;
+        }
     }
     .c-user-signature-drawing {
         padding: 18px 18px 18px 0;

--
Gitblit v1.9.1