小程序签名组件

index.wxml

<view class="wrapper" animation="{{ ami }}" wx:if="{{show}}">
    <view class="handBtn">
        <view class="pointers">
            <view class="color-pointer {{selectColor === 'black' && 'active'}}" catchtap="selectColorEvent" data-color="black" data-color-value="#1A1A1A">
            </view>
            <view class="color-pointer {{selectColor === 'red' && 'active'}}" catchtap="selectColorEvent" data-color="red" data-color-value="#ca262a"></view>
        </view>
        <view>
            <view catchtap="retDraw" class="btn delBtn">重写</view>
            <view catchtap="cancel" class="btn delBtn">取消</view>
            <view catchtap="saveCanvasAsImg" class="btn subBtn">保存</view>
        </view>
    </view>
    <view class="handCenter" id="handCenter">
        <canvas class="handWriting" disable-scroll="true" bindtouchstart="uploadScaleStart" bindtouchmove="uploadScaleMove" bindtouchend="uploadScaleEnd" bindtap="mouseDown" canvas-id="handWriting">
        </canvas>
    </view>
    <view class="handRight">
        <view class="handTitle">手写板</view>
    </view>
</view>

js

// components/um-signature/index.js
const app = getApp()
const { getNodes } = app.require('utils/index')

Component({
    /**
     * 组件的属性列表
     */
    properties: {
        show: {
            type: Boolean,
            value: false
        }
    },
    observers: {
        show(v) {
            v && this.init()
        }
    },
    /**
     * 组件的初始数据
     */
    data: {
        canvasName: 'handWriting',
        ctx: '',
        canvasWidth: 0,
        canvasHeight: 0,
        transparent: 1, // 透明度
        selectColor: 'black',
        lineColor: '#1A1A1A', // 颜色
        lineSize: 1.5,  // 笔记倍数
        lineMin: 0.5,   // 最小笔画半径
        lineMax: 4,     // 最大笔画半径
        pressure: 1,     // 默认压力
        smoothness: 60,  //顺滑度,用60的距离来计算速度
        currentPoint: {},
        currentLine: [],  // 当前线条
        firstTouch: true, // 第一次触发
        radius: 1, //画圆的半径
        cutArea: { top: 0, right: 0, bottom: 0, left: 0 }, //裁剪区域
        bethelPoint: [],  //保存所有线条 生成的贝塞尔点;
        lastPoint: 0,
        chirography: [], //笔迹
        currentChirography: {}, //当前笔迹
        linePrack: [] //划线轨迹 , 生成线条的实际点
    },

    /**
     * 组件的方法列表
     */
    methods: {
        // 笔迹开始
        uploadScaleStart(e) {
            if (e.type != 'touchstart') return false;
            let ctx = this.data.ctx;
            ctx.setFillStyle(this.data.lineColor);  // 初始线条设置颜色
            ctx.setGlobalAlpha(this.data.transparent);  // 设置半透明
            let currentPoint = {
                x: e.touches[0].x,
                y: e.touches[0].y
            }
            let currentLine = this.data.currentLine;
            currentLine.unshift({
                time: new Date().getTime(),
                dis: 0,
                x: currentPoint.x,
                y: currentPoint.y
            })
            this.setData({
                currentPoint,
                // currentLine
            })
            if (this.data.firstTouch) {
                this.setData({
                    cutArea: { top: currentPoint.y, right: currentPoint.x, bottom: currentPoint.y, left: currentPoint.x },
                    firstTouch: false
                })
            }
            this.pointToLine(currentLine);
        },
        // 笔迹移动
        uploadScaleMove(e) {
            if (e.type != 'touchmove') return false;
            if (e.cancelable) {
                // 判断默认行为是否已经被禁用
                if (!e.defaultPrevented) {
                    e.preventDefault();
                }
            }
            let point = {
                x: e.touches[0].x,
                y: e.touches[0].y
            }
            //测试裁剪
            if (point.y < this.data.cutArea.top) {
                this.data.cutArea.top = point.y;
            }
            if (point.y < 0) this.data.cutArea.top = 0;

            if (point.x > this.data.cutArea.right) {
                this.data.cutArea.right = point.x;
            }
            if (this.data.canvasWidth - point.x <= 0) {
                this.data.cutArea.right = this.data.canvasWidth;
            }
            if (point.y > this.data.cutArea.bottom) {
                this.data.cutArea.bottom = point.y;
            }
            if (this.data.canvasHeight - point.y <= 0) {
                this.data.cutArea.bottom = this.data.canvasHeight;
            }
            if (point.x < this.data.cutArea.left) {
                this.data.cutArea.left = point.x;
            }
            if (point.x < 0) this.data.cutArea.left = 0;

            this.setData({
                lastPoint: this.data.currentPoint,
                currentPoint: point
            })
            let currentLine = this.data.currentLine
            currentLine.unshift({
                time: new Date().getTime(),
                dis: this.distance(this.data.currentPoint, this.data.lastPoint),
                x: point.x,
                y: point.y
            })
            // this.setData({
            //   currentLine
            // })
            this.pointToLine(currentLine);
        },
        // 笔迹结束
        uploadScaleEnd(e) {
            if (e.type != 'touchend') return 0;
            let point = {
                x: e.changedTouches[0].x,
                y: e.changedTouches[0].y
            }
            this.setData({
                lastPoint: this.data.currentPoint,
                currentPoint: point
            })
            let currentLine = this.data.currentLine
            currentLine.unshift({
                time: new Date().getTime(),
                dis: this.distance(this.data.currentPoint, this.data.lastPoint),
                x: point.x,
                y: point.y
            })
            // this.setData({
            //   currentLine
            // })
            if (currentLine.length > 2) {
                var info = (currentLine[0].time - currentLine[currentLine.length - 1].time) / currentLine.length;
                //$("#info").text(info.toFixed(2));
            }
            //一笔结束,保存笔迹的坐标点,清空,当前笔迹
            //增加判断是否在手写区域;
            this.pointToLine(currentLine);
            var currentChirography = {
                lineSize: this.data.lineSize,
                lineColor: this.data.lineColor
            };
            var chirography = this.data.chirography
            chirography.unshift(currentChirography);
            this.setData({
                chirography
            })
            var linePrack = this.data.linePrack
            linePrack.unshift(this.data.currentLine);
            this.setData({
                linePrack,
                currentLine: []
            })
        },

        retDraw() {
            this.data.ctx.clearRect(0, 0, 700, 730)
            this.data.ctx.draw();

            //设置canvas背景
            this.setCanvasBg("#fff");
        },

        //画两点之间的线条;参数为:line,会绘制最近的开始的两个点;
        pointToLine(line) {
            this.calcBethelLine(line);
            return;
        },
        //计算插值的方式;
        calcBethelLine(line) {
            if (line.length <= 1) {
                line[0].r = this.data.radius;
                return;
            }
            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.data.radius;
            for (let n = 0; n < line.length - 1; n++) {
                dis += line[n].dis;
                time += line[n].time - line[n + 1].time;
                if (dis > this.data.smoothness) break;
            }
            this.setData({
                radius: Math.min(time / len * this.data.pressure + this.data.lineMin, this.data.lineMax) * this.data.lineSize
            });
            line[0].r = this.data.radius;
            //计算笔迹半径;
            if (line.length <= 2) {
                r0 = (lastRadius + this.data.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.data.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);
                    a[0].color = this.data.lineColor;
                    this.bethelDraw(a, 1);
                    point = [{ x: x, y: y, r: r }];
                }
            }
            this.setData({
                currentLine: line
            })
        },
        //求两点之间距离
        distance(a, b) {
            let x = b.x - a.x;
            let y = b.y - a.y;
            return Math.sqrt(x * x + y * y);
        },
        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, is_fill, color) {
            let ctx = this.data.ctx;
            ctx.beginPath();
            ctx.moveTo(point[0].mx, point[0].my);
            if (undefined != color) {
                ctx.setFillStyle(color);
                ctx.setStrokeStyle(color);
            } else {
                ctx.setFillStyle(point[0].color);
                ctx.setStrokeStyle(point[0].color);
            }
            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 (undefined != is_fill) {
                ctx.fill(); //填充图形 ( 后绘制的图形会覆盖前面的图形, 绘制时注意先后顺序 )
            }
            ctx.draw(true)
        },
        selectColorEvent(event) {
            var color = event.currentTarget.dataset.colorValue;
            var colorSelected = event.currentTarget.dataset.color;
            this.setData({
                selectColor: colorSelected,
                lineColor: color
            })
        },

        //将Canvas内容转成 临时图片 --> cb 为回调函数 形参 tempImgPath 为 生成的图片临时路径
        canvasToImg(cb) { //这种写法移动端 出不来
            this.data.ctx.draw(true, () => {
                wx.canvasToTempFilePath({
                    canvasId: 'handWriting',
                    fileType: 'png',
                    quality: 1, //图片质量
                    success(res) {
                        wx.showToast({
                            title: '执行了吗?',
                        })
                        cb(res.tempFilePath);
                    }
                })
            });
        },
        //保存到相册
        saveCanvasAsImg() {
            wx.showLoading({ title: '正在保存' })
            wx.canvasToTempFilePath({
                canvasId: 'handWriting',
                fileType: 'png',
                quality: 1, //图片质量
                success: res => {
                    console.log(res.tempFilePath)
                    wx.uploadFile({
                        // url: app.util.url("c=utility&a=file&do=upload&type=image&thumb=0&rotate=-90"),
                        filePath: res.tempFilePath,
                        name: 'file',
                        success: res => {
                            const data = JSON.parse(res.data)
                            this.triggerEvent('onSave', data)
                        },
                        complete: () => {
                            this.setData({
                                show: false
                            })
                            wx.hideLoading()
                        }
                    })
                }

            }, this)
        },

        //设置canvas背景色  不设置  导出的canvas的背景为透明 
        //@params:字符串  color
        setCanvasBg(color) {
            const { ctx, canvasWidth, canvasHeight } = this.data
            /* 将canvas背景设置为 白底,不设置  导出的canvas的背景为透明 */
            //rect() 参数说明  矩形路径左上角的横坐标,左上角的纵坐标, 矩形路径的宽度, 矩形路径的高度
            //这里是 canvasHeight - 4 是因为下边盖住边框了,所以手动减了写
            ctx.rect(0, 0, canvasWidth, canvasHeight - 4);
            ctx.setFillStyle(color)
            ctx.fill()  //设置填充
            ctx.draw()	//开画
        },
        cancel() {
            this.setData({
                show: false
            })
        },
        init() {
            // const animation = wx.createAnimation({
            //     duration: 200,
            //     timingFunction: "ease"  //动画效果
            // })
            // animation.scale(this.data.show ? 1 : 0).step()
            // this.setData({
            //     ami: animation.export()
            // })
            const { canvasName } = this.data
            let ctx = wx.createCanvasContext(canvasName, this)
            getNodes('.handCenter', this).then(res => {
                const [rect] = res
                this.setData({
                    canvasWidth: rect.width,
                    canvasHeight: rect.height,
                    ctx
                })
                this.setCanvasBg('#fff');
            })
        }
    }
})

CSS

/* pages/signature/signature.wxss */
.wrapper {
    position: fixed;
    left: 0;
    top: 0;
    width: 100vw;
    height: 100vh;
    overflow: hidden;
    display: flex;
    align-content: center;
    flex-direction: row;
    justify-content: center;
    font-size: 28rpx;
    background-color: #fbfbfb;
    z-index: 999;
    padding: 30rpx 0;
}

.handWriting {
    background: #fff;
    width: 100%;
    height: 100%;
}

.handRight {
    display: inline-flex;
    align-items: center;
}

.handCenter {
    border: 4rpx dashed #e9e9e9;
    flex: 5;
    overflow: hidden;
    box-sizing: border-box;
}

.handTitle {
    transform: rotate(90deg);
    flex: 1;
    color: #666;
}

.handBtn .btn {
    font-size: 28rpx;
    transform: rotate(90deg);
    white-space: nowrap;
    /* border: 1px solid red; */
    text-align: center;
    border-radius: 5px;
    background-color: #e6e6e6;
}

.handBtn .btn+.btn {
    margin-top: 40px;
}

.handBtn {
    height: 95vh;
    display: inline-flex;
    flex-direction: column;
    justify-content: space-between;
    align-content: space-between;
    flex: 1;
}

.delBtn {
    color: #666;
}


.subBtn {
    background: #008ef6 !important;
    color: #fff;
    margin-bottom: 30rpx;
}


.pointers {
    text-align: center;
}

.color-pointer {
    box-sizing: content-box;
    display: inline-block;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    border: 5px solid #FBFBFB;
}

.pointers .color-pointer:first-child {
    background-color: #1A1A1A;
    margin-bottom: 15px;
}

.pointers .color-pointer:last-child {
    background-color: #ca262a;
}
.pointers .color-pointer.active{
    border-color: #fff;
    box-shadow: 0 0 5px #0005;
}

在这里插入图片描述

一个小细节
在app.js 中 添加 require(uri) {
return require(uri)
},

在任何页面既可通过
const app = getApp()
通过app 获取导出的函数
const { getNodes } = app.require(‘utils/index’)

utils index.js 文件


/**
 * @desc 获取 WXML 节点信息的对象
 * @param {*} selector 选择器
 * @param {*} com 自定义组件的实例对象,如在组件内使用必传
 * @returns {promise}
 */
const getNodes = (selector, com) => {
    const query = com ? com.createSelectorQuery() : wx.createSelectorQuery()
    query.selectAll(selector).boundingClientRect()
    return new Promise((reslove, reject) => {
        query.exec(([res]) => {
            res.length ? reslove(res) : reject()
        })
    })
}
/**
 * @desc 获取某个月的天数
 * @param {Number} month 如是getMonth()获取的月份 要加1
 * @param {Number} year 
 * @returns {Number}
 */
function getDays(month = new Date().getMonth() + 1, year = new Date().getFullYear()) {
    const date = new Date(`${year}-${++month}`)
    date.setDate(0)
    return date.getDate()
}


/**
 * @desc 函数防抖
 * @param {Function} func 目标函数
 * @param {Number} delay 延迟执行毫秒数
 * @param {Boolean} immediate true - 立即执行, false - 延迟执行
 */
function debounce(func, delay = 500, immediate) {
    let timer;
    return function () {
        if (timer) clearTimeout(timer);
        if (immediate) {
            let callNow = !timer;
            timer = setTimeout(() => {
                timer = null;
            }, delay);
            if (callNow) func.apply(this, arguments);
            return
        }
        timer = setTimeout(() => {
            func.apply(this, arguments);
        }, delay)

    }
}

/**
* @desc 函数节流
* @param {Function} func 目标函数
* @param {Number} delay 延迟执行毫秒数
* @param {Boolean} immediate true - 立即执行, false - 延迟执行
*/
function throttle(func, delay = 500, immediate) {
    let timer = 0;
    return function () {
        if (immediate) {
            const now = new Date().getTime();
            if (now - timer > delay) {
                func.apply(this, arguments)
                timer = now
            }
        } else {
            if (!timer) {
                timer = setTimeout(() => {
                    timer = null;
                    func.apply(this, arguments)
                }, delay)
            }
        }

    }
}

/**
 * @desc 日期格式化,将各种日期数据类型转为指定字符串格式
 * @param {String,Number} date 日期字符串,可以是毫秒数
 * @param {String} fmt 格式化参数
 * @returns {string}
 */
function dateFormat(date, fmt = "YYYY-MM-DD hh:mm:ss") {
    if (!date) {
        return ""
    }
    if (typeof date === 'string') {
        date = date.replace("-", "/");
    }
    date = new Date(date);
    const o = {
        "M+": date.getMonth() + 1,                 //月份
        "D+": date.getDate(),                    //日
        "h+": date.getHours(),                   //小时
        "m+": date.getMinutes(),                 //分
        "s+": date.getSeconds(),                 //秒
        "q+": Math.floor((date.getMonth() + 3) / 3), //季度
        "S": date.getMilliseconds()             //毫秒
    };
    if (/(Y+)/.test(fmt)) {
        fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
    }
    for (const k in o) {
        if (new RegExp("(" + k + ")").test(fmt)) {
            fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
        }
    }
    return fmt;
}
/**
 * @desc 判断两个对象内容是否相等
 * @param {Object} a 对象1
 * @param {Object} b 对象2
 * @param {String} excludeProp 不检查某个字段
 * @returns {Boolean}
 */
function isObjectValueEqual(a, b, excludeProp) {
    var aProps = Object.getOwnPropertyNames(a).filter(x => x != excludeProp);
    var bProps = Object.getOwnPropertyNames(b).filter(x => x != excludeProp);

    if (aProps.length != bProps.length) {
        return false;
    }
    for (var i = 0; i < aProps.length; i++) {
        var propName = aProps[i]
        var propA = a[propName]
        var propB = b[propName]
        // 先判断两边都有相同键名
        if (!b.hasOwnProperty(propName)) return false
        if ((propA instanceof Object)) {
            if (isObjectValueEqual(propA, propB)) {
            } else {
                return false
            }
        } else if (propA !== propB) {
            return false
        } else { }
    }
    return true
}
/**
 * @desc 递归实现深拷贝
 * @param {*} obj 
 * @param {*} hash 
 */
function deepClone(obj, hash = new WeakMap()) {
    var cloneObj;
    var Constructor = obj.constructor
    switch (Constructor) {
        case RegExp:
            cloneObj = new Constructor(obj)
            break
        case Date:
            cloneObj = new Constructor(obj.getTime())
            break
        default:
            if (hash.has(obj)) return hash.get(obj)
            cloneObj = new Constructor()
            hash.set(obj, cloneObj)
    }
    for (const k in obj) {
        var item = obj[k]
        if (item && typeof item === 'object') {
            cloneObj[k] = deepClone(item)
        } else {
            cloneObj[k] = item
        }
    }
    return cloneObj
}

/**
 * 深度合并两个对象
 * @param obj1
 * @param obj2
 * @returns {*}
 */
function deepMerge(obj1, obj2) {
    obj1 = deepClone(obj1)
    for (const key in obj2) {
        const v1 = obj1[key]
        const v2 = obj2[key]
        if (v1 && isObject(v1) && isObject(v2)) {
            obj1[key] = deepMerge(v1, v2)
        } else {
            obj1[key] = v2
        }
    }
    return obj1
}

function isObject(obj) {
    return typeof obj == "object" && obj.constructor == Object;
}

/**
 * @desc 生成唯一标识符
 * @returns {String}
 */
function guid() {
    return 'xxxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = Math.random() * 16 | 0,
            v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
}
/**
 * @desc 递归实现数组扁平化
 * @param {Array} arr 
 * @returns {Array}
 */
function flatten(arr) {
    return arr.reduce((prev, cur) => {
        return prev.concat(Array.isArray(cur) ? flatten(cur) : cur);
    }, [])
}

/**
 * 生成指定范围随机数
 * @param n
 * @param m
 * @returns {number}
 */
export function randomByScope(n, m) {
    var result = Math.random() * (m - n) + n;
    if (result === n) {
        result = m;
    }
    return parseInt(result);
}
/**
 * @desc 数字金额大写转换
 * @param {String Number} n 
 */
function digitUppercase(n) {
    var fraction = ['角', '分'];
    var digit = [
        '零', '壹', '贰', '叁', '肆',
        '伍', '陆', '柒', '捌', '玖'
    ];
    var unit = [
        ['元', '万', '亿'],
        ['', '拾', '佰', '仟']
    ];
    var head = n < 0 ? '欠' : '';
    n = Math.abs(n);
    var s = '';
    for (var i = 0; i < fraction.length; i++) {
        s += (digit[Math.floor(n * 10 * Math.pow(10, i)) % 10] + fraction[i]).replace(/零./, '');
    }
    s = s || '整';
    n = Math.floor(n);
    for (var i = 0; i < unit[0].length && n > 0; i++) {
        var p = '';
        for (var j = 0; j < unit[1].length && n > 0; j++) {
            p = digit[n % 10] + unit[1][j] + p;
            n = Math.floor(n / 10);
        }
        s = p.replace(/(零.)*零$/, '').replace(/^$/, '零') + unit[0][i] + s;
    }
    return head + s.replace(/(零.)*零元/, '元')
        .replace(/(零.)+/g, '零')
        .replace(/^整$/, '零元整');
}

module.exports = {
    dateFormat,
    getDays,
    debounce,
    throttle,
    deepClone,
    deepMerge,
    isObjectValueEqual,
    randomByScope,
    getNodes,
    guid,
    flatten,
    digitUppercase
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在小程序中实现电子签名,可以使用小程序的 `canvas` 组件。 首先,在页面的 `wxml` 文件中添加 `canvas` 组件: ```html <canvas id="canvas" class="canvas"></canvas> ``` 然后,在页面的 `js` 文件中,获取 `canvas` 组件的上下文: ```javascript Page({ onLoad: function () { const ctx = wx.createCanvasContext('canvas'); } }) ``` 接下来,监听用户手指在 `canvas` 上的滑动事件,并将滑动轨迹绘制到 `canvas` 上: ```javascript Page({ data: { startX: 0, startY: 0, endX: 0, endY: 0 }, onLoad: function () { const ctx = wx.createCanvasContext('canvas'); ctx.setStrokeStyle('#000000'); ctx.setLineWidth(5); ctx.setLineCap('round'); ctx.setLineJoin('round'); ctx.beginPath(); ctx.draw(); }, onTouchStart: function (e) { this.setData({ startX: e.changedTouches[0].x, startY: e.changedTouches[0].y }); }, onTouchMove: function (e) { const ctx = wx.createCanvasContext('canvas'); this.setData({ endX: e.changedTouches[0].x, endY: e.changedTouches[0].y }); ctx.moveTo(this.data.startX, this.data.startY); ctx.lineTo(this.data.endX, this.data.endY); ctx.stroke(); ctx.draw(true); this.setData({ startX: this.data.endX, startY: this.data.endY }); } }) ``` 最后,将 `canvas` 组件保存为图片,并将图片保存到本地: ```javascript Page({ data: { imageSrc: '' }, onSave: function () { const ctx = wx.createCanvasContext('canvas'); wx.canvasToTempFilePath({ canvasId: 'canvas', success: (res) => { this.setData({ imageSrc: res.tempFilePath }); wx.saveImageToPhotosAlbum({ filePath: this.data.imageSrc, success: (res) => { wx.showToast({ title: '保存成功', icon: 'success', duration: 2000 }); }, fail: (err) => { wx.showToast({ title: '保存失败', icon: 'none', duration: 2000 }); } }); }, fail: (err) => { wx.showToast({ title: '保存失败', icon: 'none', duration: 2000 }); } }); } }) ``` 完整代码示例: ```html <view class="container"> <canvas id="canvas" class="canvas" bindtouchstart="onTouchStart" bindtouchmove="onTouchMove"></canvas> <view class="btn-group"> <button class="btn" bindtap="onSave">保存签名</button> </view> </view> ``` ```javascript Page({ data: { startX: 0, startY: 0, endX: 0, endY: 0, imageSrc: '' }, onLoad: function () { const ctx = wx.createCanvasContext('canvas'); ctx.setStrokeStyle('#000000'); ctx.setLineWidth(5); ctx.setLineCap('round'); ctx.setLineJoin('round'); ctx.beginPath(); ctx.draw(); }, onTouchStart: function (e) { this.setData({ startX: e.changedTouches[0].x, startY: e.changedTouches[0].y }); }, onTouchMove: function (e) { const ctx = wx.createCanvasContext('canvas'); this.setData({ endX: e.changedTouches[0].x, endY: e.changedTouches[0].y }); ctx.moveTo(this.data.startX, this.data.startY); ctx.lineTo(this.data.endX, this.data.endY); ctx.stroke(); ctx.draw(true); this.setData({ startX: this.data.endX, startY: this.data.endY }); }, onSave: function () { const ctx = wx.createCanvasContext('canvas'); wx.canvasToTempFilePath({ canvasId: 'canvas', success: (res) => { this.setData({ imageSrc: res.tempFilePath }); wx.saveImageToPhotosAlbum({ filePath: this.data.imageSrc, success: (res) => { wx.showToast({ title: '保存成功', icon: 'success', duration: 2000 }); }, fail: (err) => { wx.showToast({ title: '保存失败', icon: 'none', duration: 2000 }); } }); }, fail: (err) => { wx.showToast({ title: '保存失败', icon: 'none', duration: 2000 }); } }); } }) ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值