WebApp【公共组件库】@前端(For Git Submodule)
YFeng
2024-02-27 456b21846e549dcfed9524d1c8325c44ec6afafe
实现小程序中签名
2 files added
2 files modified
547 ■■■■■ changed files
forms/userSignature/CSignatureLayer.vue 432 ●●●●● patch | view | raw | blame | history
forms/userSignature/CUserSignature.vue 25 ●●●● patch | view | raw | blame | history
forms/userSignature/cSignatureLayer.scss 87 ●●●●● patch | view | raw | blame | history
forms/userSignature/cUserSignature.scss 3 ●●●●● patch | view | raw | blame | history
forms/userSignature/CSignatureLayer.vue
New file
@@ -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>
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);
        }
    },
};
forms/userSignature/cSignatureLayer.scss
New file
@@ -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%;
        }
    }
}
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;