Lou
2023-12-18 9559ddc77d25d049862eb0fb6bdcfa90f792830e
扫码逻辑修改,楼盘表修改
9 files modified
17 files added
2011 ■■■■■ changed files
common/setting.js 4 ●●●● patch | view | raw | blame | history
components/am-sign-input/README.md 83 ●●●●● patch | view | raw | blame | history
components/am-sign-input/am-sign-input.vue 781 ●●●●● patch | view | raw | blame | history
components/am-sign-input/pickerColor.vue 143 ●●●●● patch | view | raw | blame | history
components/am-sign-input/u-mask/u-mask.vue 124 ●●●●● patch | view | raw | blame | history
pages.json 25 ●●●●● patch | view | raw | blame | history
pages/home/index.vue 14 ●●●● patch | view | raw | blame | history
pages/user/center.vue 3 ●●●●● patch | view | raw | blame | history
static/other/color_black.png patch | view | raw | blame | history
static/other/color_black_selected.png patch | view | raw | blame | history
static/other/color_red.png patch | view | raw | blame | history
static/other/color_red_selected.png patch | view | raw | blame | history
static/other/signs.png patch | view | raw | blame | history
subPackage/article/signature.vue 44 ●●●●● patch | view | raw | blame | history
subPackage/bs/views/repair.vue 6 ●●●● patch | view | raw | blame | history
subPackage/house/family/index.vue 34 ●●●● patch | view | raw | blame | history
subPackage/house/list/index.vue 7 ●●●● patch | view | raw | blame | history
subPackage/house/roomDetails/index.vue 43 ●●●● patch | view | raw | blame | history
subPackage/label/index.vue 6 ●●●● patch | view | raw | blame | history
uni_modules/jushi-signature/changelog.md 4 ●●●● patch | view | raw | blame | history
uni_modules/jushi-signature/components/image-tools/README.md 76 ●●●●● patch | view | raw | blame | history
uni_modules/jushi-signature/components/image-tools/index.js 196 ●●●●● patch | view | raw | blame | history
uni_modules/jushi-signature/components/image-tools/package.json 25 ●●●●● patch | view | raw | blame | history
uni_modules/jushi-signature/components/jushi-signature/jushi-signature.vue 228 ●●●●● patch | view | raw | blame | history
uni_modules/jushi-signature/package.json 83 ●●●●● patch | view | raw | blame | history
uni_modules/jushi-signature/readme.md 82 ●●●●● patch | view | raw | blame | history
common/setting.js
@@ -13,8 +13,8 @@
    // devUrl: 'http://192.168.1.156:9528',
    // devUrl:'http://192.168.1.50:9528',
    // devUrl: 'http://192.168.0.102:9528',
    // devUrl:'https://srgdjczzxtpt.com:2080/api',
    devUrl: 'http://192.168.0.103:9528',
    devUrl:'https://srgdjczzxtpt.com:2080/api',
    // devUrl: 'http://192.168.0.103:9528',
    // devUrl: 'https://srgdjczzxtpt.com:2080/api',
    minioBaseUrl: "https://srgdjczzxtpt.com:2080/gminio/jczz/",
    // minioBaseUrl:"http://192.168.0.103:9528/",
components/am-sign-input/README.md
New file
@@ -0,0 +1,83 @@
### 使用方法
* 注意
+ 同一个页面不同的输入框需要设置不同的canvasId和canvasIds,否则在同一个页面会出现冲突
```
<template>
    <view class="content">
        <signInput ref="sign" canvasId="twoDrowCanvas" canvasIds="twoRotateCanvas" :header="header" :action="action"
            @signToUrl="signToUrl">
        </signInput>
    </view>
</template>
```
```
<script>
    import signInput from "@/components/am-sign-input/am-sign-input.vue"
    export default {
        components: {
            signInput
        },
        data() {
            return {
                action: "", //上传服务器的地址
                header: {}, //图片上传携带头部信息
            }
        },
        methods: {
            /**
             * @param {Object} e
             * 签名完成回调
             */
            signToUrl(e) {
                if (e.error_code && e.error_code === '201') {
                    uni.showToast({
                        title: e.msg,
                        icon: 'none'
                    })
                    return
                }
            },
        }
    }
</script>
```
```
<style lang="scss">
    .content {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
    }
</style>
```
#### 实际效果演示H5
- 打开演示后,按F12调整到手机调试模式查看效果
[实际效果演示](https://static-mp-2766f90e-0e50-4c71-87fb-0ab51aedcf85.next.bspapp.com/signInput/#/)
### 参数说明Props
参数|类型|说明|必传
---|---|---|---
action|String|生成图片后上传的接口地址|true
canvasId|String|canvasId|true
canvasIds|String|canvasIds与上一个id不可重复|true
header|Object|文件上传携带的头部属性|true
outSignWidth|Number|输出图片文件大小-宽度|false
outSignHeight|Number|输出图片文件大小-高度|false
minSpeed|Number|画笔最小速度|false
minWidth|Number|线条最小粗度|false
maxWidth|Number|线条最大粗度|false
openSmooth|Boolean|开启平滑线条(笔锋)|false
maxHistoryLength|Number|历史最大长度(用于撤销的步数)|false
maxWidthDiffRate|Number|最大差异率|false
undoScan|Number|撤销重新渲染偏移缩放校准|false
bgColor|String|背景色如#ffffff 不传则为透明|false
### 相关同源插件
- 以页面形式展现
- [电子签名组件](https://ext.dcloud.net.cn/plugin?id=5768)
### 相关致谢
- 插件参考 [大佬的npm库](https://github.com/linjc/smooth-signature)
components/am-sign-input/am-sign-input.vue
New file
@@ -0,0 +1,781 @@
<template>
    <view class="sign">
        <view class="imgBox">
            <view class="nom_img" v-if="!showImg" @click="signModShow=true">
                <image v-if="!showImg" src="/static/other/signs.png" style="width: 34px;height: 34px;">
                </image>
            </view>
            <view class="across_img" v-if="showImg">
                <view v-if="showImg" class="delete_icon" @click.stop="deleteImg">
                    x
                </view>
                <image v-if="showImg" :src="showImg" style="width: 140px;height: 80px;" @click="previewImg(showImg)">
                </image>
            </view>
        </view>
        <umask :show="signModShow" @click="signModShow=false" :duration="0">
            <view class="warp">
                <view class="signBox" @tap.stop>
                    <view class="wrapper">
                        <view class="handBtn">
                            <!-- #ifdef MP-WEIXIN -->
                            <image @click="selectColorEvent('black','#1A1A1A')"
                                :src="selectColor === 'black' ? '/static/other/color_black_selected.png' : '/static/other/color_black.png'"
                                class="black-select"></image>
                            <image @click="selectColorEvent('red','#ca262a')"
                                :src="selectColor === 'red' ? '/static/other/color_red_selected.png' : '/static/other/color_red.png'"
                                class="red-select"></image>
                            <!-- #endif -->
                            <!-- #ifndef MP-WEIXIN -->
                            <view class="color_pic" :style="{background:lineColor}" @click="showPickerColor=true">
                            </view>
                            <!-- #endif -->
                            <button @click="clear" class="delBtn">清空</button>
                            <button @click="saveCanvasAsImg" class="saveBtn">保存</button>
                            <button @click="previewCanvasImg" class="previewBtn">预览</button>
                            <button @click="subCanvas" class="subBtn">完成</button>
                            <button @click="undo" class="undoBtn">撤销</button>
                            <span class="emptyInfo" style="color: red;" v-if="emptyShow">你还没有绘制任何东西哦</span>
                        </view>
                        <view class="handCenter" :style="{left:canvasLeft+'px'}">
                            <canvas :disable-scroll="true" @touchstart="uploadScaleStart" @touchmove="uploadScaleMove"
                                @touchend="uploadScaleEnd" :style="{width:'100%',height:'calc(85vh - 8rpx)'}"
                                :canvas-id="canvasId"></canvas>
                        </view>
                        <view class="handCenters">
                            <canvas :canvas-id="canvasIds"
                                :style="{width:outSignWidth+'px',height:outSignHeight+'px'}"></canvas>
                        </view>
                        <view class="handRight">
                            <view class="handTitle">请签名
                            </view>
                        </view>
                    </view>
                </view>
            </view>
        </umask>
        <pickerColor :isShow="showPickerColor" :bottom="0" @callback='getPickerColor' />
    </view>
</template>
<script>
    import umask from "./u-mask/u-mask.vue"
    import pickerColor from "./pickerColor.vue"
    export default {
        components: {
            umask,
            pickerColor
        },
        data() {
            return {
                canvasLeft: 10000,
                emptyShow: false,
                signModShow: false,
                showImg: "",
                showPickerColor: false,
                ctx: '',
                ctxs: '',
                canvasWidth: 0,
                canvasHeight: 0,
                selectColor: 'black',
                lineColor: '#1A1A1A',
                points: [],
                historyList: [],
                canAddHistory: true,
                getImagePath: () => {
                    let that = this
                    return new Promise((resolve) => {
                        uni.canvasToTempFilePath({
                            canvasId: that.canvasId,
                            fileType: 'png',
                            quality: 1, //图片质量
                            success: res => resolve(res.tempFilePath)
                        }, this)
                    })
                },
                requestAnimationFrame: void 0,
            };
        },
        watch: {
            signModShow(newValue, oldValue) {
                newValue ? this.canvasLeft = 74 : this.canvasLeft = 10000
            }
        },
        props: { //可用于修改的参数放在props里   也可单独放在外面做成组件调用  传值
            action: {
                type: String,
                default: ''
            },
            canvasId: {
                type: String,
                default: 'canvasDr'
            },
            canvasIds: {
                type: String,
                default: 'canvasRo'
            },
            header: {
                type: Object,
                default: {}
            },
            outSignWidth: {
                type: Number,
                default: 54
            },
            outSignHeight: {
                type: Number,
                default: 24
            },
            minSpeed: { //画笔最小速度
                type: Number,
                default: 1.5
            },
            minWidth: { //线条最小粗度
                type: Number,
                default: 3,
            },
            maxWidth: { //线条最大粗度
                type: Number,
                default: 10
            },
            openSmooth: { //开启平滑线条(笔锋)
                type: Boolean,
                default: true
            },
            maxHistoryLength: { //历史最大长度
                type: Number,
                default: 20
            },
            maxWidthDiffRate: { //最大差异率
                type: Number,
                default: 20
            },
            undoScan: { //撤销重新渲染偏移缩放校准
                type: Number,
                default: 0.83
            },
            bgColor: { //背景色
                type: String,
                default: ''
            },
        },
        mounted() {
            if (!this.ctx) {
                this.ctx = uni.createCanvasContext(this.canvasId, this);
            }
            if (!this.ctxs) {
                this.ctxs = uni.createCanvasContext(this.canvasIds, this);
            }
            let that = this
            this.$nextTick(() => {
                uni.createSelectorQuery().in(this).select('.handCenter').boundingClientRect(rect => {
                        that.canvasWidth = rect.width;
                        that.canvasHeight = rect.height;
                        that.drawBgColor()
                    })
                    .exec();
            })
        },
        methods: {
            getPickerColor(color) {
                this.showPickerColor = false;
                if (color) {
                    this.lineColor = color;
                }
            },
            // 笔迹开始
            uploadScaleStart(e) {
                this.canAddHistory = true
                this.ctx.setStrokeStyle(this.lineColor)
                this.ctx.setLineCap("round") //'butt'、'round'、'square'
            },
            // 笔迹移动
            uploadScaleMove(e) {
                let temX = e.changedTouches[0].x
                let temY = e.changedTouches[0].y
                this.initPoint(temX, temY)
                this.onDraw()
            },
            /**
             * 触摸结束
             */
            uploadScaleEnd() {
                this.canAddHistory = true;
                this.points = [];
            },
            /**
             * 记录点属性
             */
            initPoint(x, y) {
                var point = {
                    x: x,
                    y: y,
                    t: Date.now()
                };
                var prePoint = this.points.slice(-1)[0];
                if (prePoint && (prePoint.t === point.t || prePoint.x === x && prePoint.y === y)) {
                    return;
                }
                if (prePoint && this.openSmooth) {
                    var prePoint2 = this.points.slice(-2, -1)[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);
                    point.lineWidth = this.getLineWidth(point.speed);
                    if (prePoint2 && prePoint2.lineWidth && prePoint.lineWidth) {
                        var rate = (point.lineWidth - prePoint.lineWidth) / prePoint.lineWidth;
                        var maxRate = this.maxWidthDiffRate / 100;
                        maxRate = maxRate > 1 ? 1 : maxRate < 0.01 ? 0.01 : maxRate;
                        if (Math.abs(rate) > maxRate) {
                            var per = rate > 0 ? maxRate : -maxRate;
                            point.lineWidth = prePoint.lineWidth * (1 + per);
                        }
                    }
                }
                this.points.push(point);
                this.points = this.points.slice(-3);
            },
            /**
             * @param {Object}
             * 线宽
             */
            getLineWidth(speed) {
                var minSpeed = this.minSpeed > 10 ? 10 : this.minSpeed < 1 ? 1 : this.minSpeed; //1.5
                var addWidth = (this.maxWidth - this.minWidth) * speed / minSpeed;
                var lineWidth = Math.max(this.maxWidth - addWidth, this.minWidth);
                return Math.min(lineWidth, this.maxWidth);
            },
            /**
             * 绘画逻辑
             */
            onDraw() {
                if (this.points.length < 2) return;
                this.addHistory();
                var point = this.points.slice(-1)[0];
                var prePoint = this.points.slice(-2, -1)[0];
                let that = this
                var onDraw = function onDraw() {
                    if (that.openSmooth) {
                        that.drawSmoothLine(prePoint, point);
                    } else {
                        that.drawNoSmoothLine(prePoint, point);
                    }
                };
                if (typeof this.requestAnimationFrame === 'function') {
                    this.requestAnimationFrame(function() {
                        return onDraw();
                    });
                } else {
                    onDraw();
                }
            },
            //添加历史图片地址
            addHistory() {
                if (!this.maxHistoryLength || !this.canAddHistory) return;
                this.canAddHistory = false;
                if (!this.getImagePath) {
                    this.historyList.length++;
                    return;
                }
                //历史地址 (暂时无用)
                let that = this
                that.getImagePath().then(function(url) {
                    if (url) {
                        that.historyList.push(url)
                        that.historyList = that.historyList.slice(-that.maxHistoryLength);
                    }
                });
            },
            //画平滑线
            drawSmoothLine(prePoint, point) {
                var dis_x = point.x - prePoint.x;
                var dis_y = point.y - prePoint.y;
                if (Math.abs(dis_x) + Math.abs(dis_y) <= 2) {
                    point.lastX1 = point.lastX2 = prePoint.x + dis_x * 0.5;
                    point.lastY1 = point.lastY2 = prePoint.y + dis_y * 0.5;
                } else {
                    point.lastX1 = prePoint.x + dis_x * 0.3;
                    point.lastY1 = prePoint.y + dis_y * 0.3;
                    point.lastX2 = prePoint.x + dis_x * 0.7;
                    point.lastY2 = prePoint.y + dis_y * 0.7;
                }
                point.perLineWidth = (prePoint.lineWidth + point.lineWidth) / 2;
                if (typeof prePoint.lastX1 === 'number') {
                    this.drawCurveLine(prePoint.lastX2, prePoint.lastY2, prePoint.x, prePoint.y, point.lastX1, point
                        .lastY1, point.perLineWidth);
                    if (prePoint.isFirstPoint) return;
                    if (prePoint.lastX1 === prePoint.lastX2 && prePoint.lastY1 === prePoint.lastY2) return;
                    var data = this.getRadianData(prePoint.lastX1, prePoint.lastY1, prePoint.lastX2, prePoint.lastY2);
                    var points1 = this.getRadianPoints(data, prePoint.lastX1, prePoint.lastY1, prePoint.perLineWidth / 2);
                    var points2 = this.getRadianPoints(data, prePoint.lastX2, prePoint.lastY2, point.perLineWidth / 2);
                    this.drawTrapezoid(points1[0], points2[0], points2[1], points1[1]);
                } else {
                    point.isFirstPoint = true;
                }
            },
            //画不平滑线
            drawNoSmoothLine(prePoint, point) {
                point.lastX = prePoint.x + (point.x - prePoint.x) * 0.5;
                point.lastY = prePoint.y + (point.y - prePoint.y) * 0.5;
                if (typeof prePoint.lastX === 'number') {
                    this.drawCurveLine(prePoint.lastX, prePoint.lastY, prePoint.x, prePoint.y, point.lastX, point.lastY,
                        this.maxWidth);
                }
            },
            //画线
            drawCurveLine(x1, y1, x2, y2, x3, y3, lineWidth) {
                lineWidth = Number(lineWidth.toFixed(1));
                this.ctx.setLineWidth && this.ctx.setLineWidth(lineWidth);
                this.ctx.lineWidth = lineWidth;
                this.ctx.beginPath();
                this.ctx.moveTo(Number(x1.toFixed(1)), Number(y1.toFixed(1)));
                this.ctx.quadraticCurveTo(Number(x2.toFixed(1)), Number(y2.toFixed(1)), Number(x3.toFixed(1)), Number(y3
                    .toFixed(1)));
                this.ctx.stroke();
                this.ctx.draw && this.ctx.draw(true);
            },
            //画梯形
            drawTrapezoid(point1, point2, point3, point4) {
                this.ctx.beginPath();
                this.ctx.moveTo(Number(point1.x.toFixed(1)), Number(point1.y.toFixed(1)));
                this.ctx.lineTo(Number(point2.x.toFixed(1)), Number(point2.y.toFixed(1)));
                this.ctx.lineTo(Number(point3.x.toFixed(1)), Number(point3.y.toFixed(1)));
                this.ctx.lineTo(Number(point4.x.toFixed(1)), Number(point4.y.toFixed(1)));
                this.ctx.setFillStyle && this.ctx.setFillStyle(this.lineColor);
                this.ctx.fillStyle = this.lineColor;
                this.ctx.fill();
                this.ctx.draw && this.ctx.draw(true);
            },
            //获取弧度
            getRadianData(x1, y1, x2, y2) {
                var dis_x = x2 - x1;
                var dis_y = y2 - y1;
                if (dis_x === 0) {
                    return {
                        val: 0,
                        pos: -1
                    };
                }
                if (dis_y === 0) {
                    return {
                        val: 0,
                        pos: 1
                    };
                }
                var val = Math.abs(Math.atan(dis_y / dis_x));
                if (x2 > x1 && y2 < y1 || x2 < x1 && y2 > y1) {
                    return {
                        val: val,
                        pos: 1
                    };
                }
                return {
                    val: val,
                    pos: -1
                };
            },
            //获取弧度点
            getRadianPoints(radianData, x, y, halfLineWidth) {
                if (radianData.val === 0) {
                    if (radianData.pos === 1) {
                        return [{
                            x: x,
                            y: y + halfLineWidth
                        }, {
                            x: x,
                            y: y - halfLineWidth
                        }];
                    }
                    return [{
                        y: y,
                        x: x + halfLineWidth
                    }, {
                        y: y,
                        x: x - halfLineWidth
                    }];
                }
                var dis_x = Math.sin(radianData.val) * halfLineWidth;
                var dis_y = Math.cos(radianData.val) * halfLineWidth;
                if (radianData.pos === 1) {
                    return [{
                        x: x + dis_x,
                        y: y + dis_y
                    }, {
                        x: x - dis_x,
                        y: y - dis_y
                    }];
                }
                return [{
                    x: x + dis_x,
                    y: y - dis_y
                }, {
                    x: x - dis_x,
                    y: y + dis_y
                }];
            },
            /**
             * 背景色
             */
            drawBgColor() {
                if (!this.bgColor) return;
                this.ctx.setFillStyle && this.ctx.setFillStyle(this.bgColor);
                this.ctx.fillStyle = this.bgColor;
                this.ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
                this.ctx.draw && this.ctx.draw(true);
            },
            //图片绘制
            drawByImage(url) {
                this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
                try {
                    this.ctx.drawImage(url, 0, 0, this.canvasWidth * this.undoScan, this.canvasHeight * this.undoScan);
                    this.ctx.draw && this.ctx.draw(true);
                } catch (e) {
                    this.historyList.length = 0;
                }
            },
            /**
             * 清空
             */
            clear() {
                this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
                this.ctx.draw && this.ctx.draw();
                this.drawBgColor();
                this.historyList.length = 0;
            },
            //撤消
            undo() {
                if (!this.getImagePath || !this.historyList.length) return;
                var pngURL = this.historyList.splice(-1)[0];
                this.drawByImage(pngURL);
                if (this.historyList.length === 0) {
                    this.clear();
                }
            },
            //是否为空
            isEmpty() {
                return this.historyList.length === 0;
            },
            /**
             * @param {Object} str
             * @param {Object} color
             * 选择颜色
             */
            selectColorEvent(str, color) {
                this.selectColor = str;
                this.lineColor = color;
                this.ctx.setStrokeStyle(this.lineColor)
            },
            //完成
            subCanvas() {
                let that = this
                if (that.isEmpty()) {
                    that.emptyShow = true
                    setTimeout(function() {
                        that.emptyShow = false
                    }, 1000)
                    return
                }
                uni.canvasToTempFilePath({
                    canvasId: that.canvasId,
                    fileType: 'png',
                    quality: 1, //图片质量
                    success(res) {
                        that.ctxs.translate(0, that.outSignHeight);
                        that.ctxs.rotate(-90 * Math.PI / 180)
                        that.ctxs.drawImage(res.tempFilePath, 0, 0, that.outSignHeight, that.outSignWidth)
                        that.ctxs.draw()
                        setTimeout(() => {
                            uni.canvasToTempFilePath({
                                canvasId: that.canvasIds,
                                fileType: 'png',
                                quality: 1, //图片质量
                                success: function(res1) {
                                    if (that.action) {
                                        uni.showLoading()
                                        uni.uploadFile({
                                            url: that.action, //图片上传post请求的地址
                                            filePath: res1.tempFilePath,
                                            name: "file",
                                            header: that.header,
                                            success: (uploadFileRes) => {
                                                uni.hideLoading()
                                                that.showImg = res1.tempFilePath
                                                that.$emit('signToUrl',
                                                    uploadFileRes)
                                                that.signModShow = false
                                                that.clear()
                                            },
                                            fail: (error) => {
                                                uni.hideLoading()
                                            }
                                        });
                                    } else {
                                        that.showImg = res1.tempFilePath
                                        that.$emit('signToUrl', {
                                            error_code: "201",
                                            msg: "请配置上传文件接口参数action"
                                        })
                                        that.signModShow = false
                                        that.clear()
                                    }
                                },
                                fail: (err) => {}
                            }, that)
                        }, 200);
                    }
                }, this);
            },
            //保存到相册
            saveCanvasAsImg() {
                uni.canvasToTempFilePath({
                    canvasId: this.canvasId,
                    fileType: 'png',
                    quality: 1, //图片质量
                    success(res) {
                        uni.saveImageToPhotosAlbum({
                            filePath: res.tempFilePath,
                            success(res) {
                                uni.showToast({
                                    title: '已保存到相册',
                                    duration: 2000
                                });
                            }
                        });
                    }
                }, this);
            },
            //预览
            previewCanvasImg() {
                uni.canvasToTempFilePath({
                    canvasId: this.canvasId,
                    fileType: 'jpg',
                    quality: 1, //图片质量
                    success(res) {
                        uni.previewImage({
                            urls: [res.tempFilePath] //预览图片 数组
                        });
                    }
                }, this);
            },
            deleteImg() {
                this.showImg = ""
            },
            previewImg(img) {
                uni.previewImage({
                    urls: [img] //预览图片 数组
                });
            },
        }
    };
</script>
<style lang="scss">
    page {
        background: #d9d9d9;
        height: auto;
        overflow: hidden;
    }
    .imgBox {
        width: 140px;
        height: 80px;
        position: relative;
        .nom_img {
            border-radius: 8px;
            border: 1px dashed;
            border-color: #a3a3a3;
            overflow: hidden;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 80px;
            width: 140px;
        }
        .nom_img:hover {
            border-color: #008ef6 !important;
        }
        .across_img {
            border: 1px dashed #a3a3a3;
            border-radius: 8px;
            height: 80px;
            width: 140px;
            .delete_icon {
                position: absolute;
                top: -12px;
                right: -12px;
                width: 24px;
                height: 24px;
                overflow: hidden;
                color: #ffffff;
                font-size: 24px;
                text-align: center;
                line-height: 20px;
                background: #ff3c0c;
                border-radius: 25px;
                z-index: 1;
            }
        }
    }
    .warp {
        width: 100%;
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
        .signBox {
            width: 85vw;
            height: 85vh;
            background: #ffffff;
            border-radius: 8px;
        }
    }
    .wrapper {
        width: 85vw;
        height: 85vh;
        overflow: hidden;
        display: flex;
        align-content: center;
        flex-direction: row;
        justify-content: center;
        font-size: 28rpx;
    }
    .handRight {
        display: inline-flex;
        align-items: center;
    }
    .handCenter {
        position: fixed;
        border: 4rpx dashed #e9e9e9;
        flex: 5;
        margin-top: 4rpx;
        overflow: hidden;
        box-sizing: border-box;
        width: calc(100% - 84rpx - 200rpx);
        height: calc(85vh - 8rpx)
    }
    .handCenters {
        position: fixed;
        top: 0;
        left: 10000rpx;
        flex: 5;
        overflow: hidden;
        box-sizing: border-box;
    }
    .handTitle {
        transform: rotate(90deg);
        flex: 1;
        color: #666;
    }
    .handBtn button {
        font-size: 28rpx;
    }
    .handBtn {
        height: 85vh;
        display: inline-flex;
        flex-direction: column;
        justify-content: space-between;
        align-content: space-between;
        flex: 1;
    }
    .delBtn {
        position: absolute;
        top: 380rpx;
        left: 46rpx;
        transform: rotate(90deg);
        color: #666;
    }
    .subBtn {
        position: absolute;
        bottom: 158rpx;
        left: 46rpx;
        display: inline-flex;
        transform: rotate(90deg);
        background: #008ef6;
        color: #fff;
        text-align: center;
        justify-content: center;
    }
    /*Peach - 新增 - 保存*/
    .saveBtn {
        position: absolute;
        top: 650rpx;
        left: 46rpx;
        transform: rotate(90deg);
        color: #666;
    }
    .previewBtn {
        position: absolute;
        top: 516rpx;
        left: 46rpx;
        transform: rotate(90deg);
        color: #666;
    }
    .undoBtn {
        position: absolute;
        top: 780rpx;
        left: 46rpx;
        transform: rotate(90deg);
        color: #666;
    }
    .emptyInfo {
        position: absolute;
        bottom: 418rpx;
        left: -56rpx;
        transform: rotate(90deg);
        color: #666;
    }
    .color_pic {
        width: 70rpx;
        height: 70rpx;
        border-radius: 25px;
        position: absolute;
        top: 200rpx;
        left: 62rpx;
        border: 1px solid #ddd;
    }
    /*Peach - 新增 - 保存*/
    .black-select {
        width: 60rpx;
        height: 60rpx;
        position: absolute;
        top: 150rpx;
        left: 70rpx;
    }
    .red-select {
        width: 60rpx;
        height: 60rpx;
        position: absolute;
        top: 260rpx;
        left: 70rpx;
    }
</style>
components/am-sign-input/pickerColor.vue
New file
@@ -0,0 +1,143 @@
<template>
    <view v-show="isShow">
        <view class="shade" @tap="hide">
            <view class="pop">
                <view class="list flex_col" v-for="(item,index) in colorArr" :key="index">
                    <view v-for="(v,i) in item" :key="i" :style="{'backgroundColor':v}" :data-color="v"
                        :data-index="index" :data-i="i" :class="{'active':(index==pickerArr[0] && i==pickerArr[1])}"
                        @tap.stop="picker"></view>
                </view>
            </view>
        </view>
    </view>
</template>
<script>
    export default {
        name: 'picker-color',
        props: {
            isShow: {
                type: Boolean,
                default: false,
            },
            bottom: {
                type: Number,
                default: 0,
            }
        },
        data() {
            return {
                colorArr: [
                    ['#000000', '#111111', '#222222', '#333333', '#444444', '#666666', '#999999', '#CCCCCC', '#EEEEEE',
                        '#FFFFFF'
                    ],
                    ['#ff0000', '#ff0033', '#ff3399', '#ff33cc', '#cc00ff', '#9900ff', '#cc00cc', '#cc0099', '#cc3399',
                        '#cc0066'
                    ],
                    ['#cc3300', '#cc6600', '#ff9933', '#ff9966', '#ff9999', '#ff99cc', '#ff99ff', '#cc66ff', '#9966ff',
                        '#cc33ff'
                    ],
                    ['#663300', '#996600', '#996633', '#cc9900', '#a58800', '#cccc00', '#ffff66', '#ffff99', '#ffffcc',
                        '#ffcccc'
                    ],
                    ['#336600', '#669900', '#009900', '#009933', '#00cc00', '#66ff66', '#339933', '#339966', '#009999',
                        '#33cccc'
                    ],
                    ['#003366', '#336699', '#3366cc', '#0099ff', '#000099', '#0000cc', '#660066', '#993366', '#993333',
                        '#800000'
                    ]
                ],
                pickerColor: '',
                pickerArr: [-1, -1]
            };
        },
        methods: {
            picker(e) {
                let data = e.currentTarget.dataset;
                this.pickerColor = data.color;
                this.pickerArr = [data.index, data.i];
                this.$emit("callback", this.pickerColor);
            },
            hide() {
                this.$emit("callback", '');
            },
        },
    }
</script>
<style scoped>
    .shade {
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        background-color: rgba(0, 0, 0, 0.5);
        z-index: 10080;
        display: flex;
        justify-content: center;
        align-items: center
    }
    .pop {
        border-radius: 8px;
        background-color: #fff;
        z-index: 100;
        padding: 12upx;
        font-size: 32upx;
        transform: rotate(90deg);
    }
    .flex_col {
        display: flex;
        flex-direction: row;
        flex-wrap: nowrap;
        justify-content: flex-start;
        align-items: center;
        align-content: center;
    }
    .list {
        justify-content: space-between;
    }
    .list>view {
        width: 60upx;
        height: 60upx;
        margin: 5upx;
        box-sizing: border-box;
        border-radius: 3px;
        box-shadow: 0 0 2px #ccc;
    }
    .list .active {
        box-shadow: 0 0 2px #09f;
        transform: scale(1.05, 1.05);
    }
    .preview {
        width: 180upx;
        height: 60upx;
    }
    .value {
        margin: 0 40upx;
        flex-grow: 1;
    }
    .ok {
        width: 160upx;
        height: 60upx;
        line-height: 60upx;
        text-align: center;
        background-color: #ff9933;
        color: #fff;
        border-radius: 4px;
        letter-spacing: 3px;
        font-size: 32upx;
    }
    .ok:active {
        background-color: rgb(255, 107, 34);
    }
</style>
components/am-sign-input/u-mask/u-mask.vue
New file
@@ -0,0 +1,124 @@
<template>
    <view class="u-mask" hover-stop-propagation :style="[maskStyle, zoomStyle]" @tap="click"
        @touchmove.stop.prevent="() => {}" :class="{
        'u-mask-zoom': zoom,
        'u-mask-show': show
    }">
        <slot />
    </view>
</template>
<script>
    /**
     * mask 遮罩
     * @description 创建一个遮罩层,用于强调特定的页面元素,并阻止用户对遮罩下层的内容进行操作,一般用于弹窗场景
     * @tutorial https://www.uviewui.com/components/mask.html
     * @property {Boolean} show 是否显示遮罩(默认false)
     * @property {String Number} z-index z-index 层级(默认1070)
     * @property {Object} custom-style 自定义样式对象,见上方说明
     * @property {String Number} duration 动画时长,单位毫秒(默认300)
     * @property {Boolean} zoom 是否使用scale对遮罩进行缩放(默认true)
     * @property {Boolean} mask-click-able 遮罩是否可点击,为false时点击不会发送click事件(默认true)
     * @event {Function} click mask-click-able为true时,点击遮罩发送此事件
     * @example <u-mask :show="show" @click="show = false"></u-mask>
     */
    export default {
        name: "u-mask",
        props: {
            // 是否显示遮罩
            show: {
                type: Boolean,
                default: false
            },
            // 层级z-index
            zIndex: {
                type: [Number, String],
                default: '10070'
            },
            // 用户自定义样式
            customStyle: {
                type: Object,
                default () {
                    return {}
                }
            },
            // 遮罩的动画样式, 是否使用使用zoom进行scale进行缩放
            zoom: {
                type: Boolean,
                default: true
            },
            // 遮罩的过渡时间,单位为ms
            duration: {
                type: [Number, String],
                default: 300
            },
            // 是否可以通过点击遮罩进行关闭
            maskClickAble: {
                type: Boolean,
                default: true
            }
        },
        data() {
            return {
                zoomStyle: {
                    transform: ''
                },
                scale: 'scale(1.2, 1.2)'
            }
        },
        watch: {
            show(n) {
                if (n && this.zoom) {
                    // 当展示遮罩的时候,设置scale为1,达到缩小(原来为1.2)的效果
                    this.zoomStyle.transform = 'scale(1, 1)';
                } else if (!n && this.zoom) {
                    // 当隐藏遮罩的时候,设置scale为1.2,达到放大(因为显示遮罩时已重置为1)的效果
                    this.zoomStyle.transform = this.scale;
                }
            }
        },
        computed: {
            maskStyle() {
                let style = {};
                style.backgroundColor = "rgba(0, 0, 0, 0.6)";
                if (this.show) style.zIndex = this.zIndex ? this.zIndex : this.$u.zIndex.mask;
                else style.zIndex = -1;
                style.transition = `all ${this.duration / 1000}s ease-in-out`;
                // 判断用户传递的对象是否为空,不为空就进行合并
                if (Object.keys(this.customStyle).length) style = {
                    ...style,
                    ...this.customStyle
                };
                return style;
            }
        },
        methods: {
            click() {
                if (!this.maskClickAble) return;
                this.$emit('click');
            }
        }
    }
</script>
<style lang="scss" scoped>
    // @import "../../libs/css/style.components.scss";
    .u-mask {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        opacity: 0;
        transition: transform 0.3s;
    }
    .u-mask-show {
        opacity: 1;
    }
    .u-mask-zoom {
        transform: scale(1.2, 1.2);
    }
</style>
pages.json
@@ -189,6 +189,15 @@
                        "navigationBarBackgroundColor": "#fff",
                        "navigationBarTextStyle": "black"
                    }
                },
                {
                    "path": "signature",
                    "style": {
                        "navigationBarTitleText": "签名",
                        "enablePullDownRefresh": false,
                        "navigationBarBackgroundColor": "#fff",
                        "navigationBarTextStyle": "black"
                    }
                }
            ]
@@ -213,11 +222,11 @@
            "pages": [{
                    "path": "index",
                    "style": {
                        "navigationBarTitleText": "标签事件",
                        "navigationBarTitleText": "标签报事",
                        "enablePullDownRefresh": false,
                        "navigationBarBackgroundColor": "#4586fe",
                        "navigationBarTextStyle": "white",
                        "navigationStyle": "custom"
                        "navigationBarTextStyle": "white"
                        // "navigationStyle": "custom"
                    }
                },
                {
@@ -517,11 +526,11 @@
                {
                    "path": "views/repair",
                    "style": {
                        "navigationBarTitleText": "报事报修",
                        "navigationBarBackgroundColor": "#fff",
                        "navigationBarTextStyle": "black",
                        "enablePullDownRefresh": false,
                        "navigationStyle": "custom"
                        "navigationBarTitleText": "公共报事",
                        "navigationBarBackgroundColor": "#4586fe",
                        "navigationBarTextStyle": "white",
                        "enablePullDownRefresh": false
                        // "navigationStyle": "custom"
                    }
                },
                {
pages/home/index.vue
@@ -764,13 +764,19 @@
                    addressCode:code
                }).then(res=>{
                    console.log(res)
                    if(res.data.doorplateType == "户室牌"){
                        this.$u.func.globalNavigator(`/subPackage/house/roomDetails/index?id=${code}`,"navTo")
                    }else if (res.data.addressLevel == 4){
                    let { doorplateType, addressLevel,aoiName,addressName,neiName } = res.data;
                    if(doorplateType == "户室牌"){
                        this.$u.func.globalNavigator(`/subPackage/house/roomDetails/index?id=${code}&from=scan`,"navTo")
                    }else if (addressLevel == 4){
                        this.$u.func.globalNavigator(
                            `/subPackage/house/houseNumber/index?stdId=${code}`, "navTo")
                    }else if (doorplateType == "楼幢牌"){
                        let url = "/subPackage/house/family/index"
                        this.$u.func.globalNavigator(`${url}?id=${code}&address=${addressName}&neiName=${neiName}&aoiName=${aoiName}`,"navTo")
                    }else if (doorplateType  =="大门牌"){
                        this.$u.func.globalNavigator(`/subPackage/house/list/index?id=${code}&title=${aoiName}`,"navTo")
                    }
                })
                })
            }
        }
    }
pages/user/center.vue
@@ -64,6 +64,9 @@
                <u-cell title="修改密码"  :border="false"   isLink  url="/subPackage/user/password/index">
                    <image slot="icon" src="/static/icon/menu-center-03.png" class="icon" mode=""></image>
                </u-cell>
                <u-cell title="签名"  :border="false"   isLink  url="/subPackage/article/signature">
                    <image slot="icon" src="/static/icon/menu-center-03.png" class="icon" mode=""></image>
                </u-cell>
                <!-- <u-cell-item title="评分">
                    <image slot="icon" src="/static/images/user/c5.png" class="icon" mode=""></image>
                </u-cell-item>
static/other/color_black.png
static/other/color_black_selected.png
static/other/color_red.png
static/other/color_red_selected.png
static/other/signs.png
subPackage/article/signature.vue
New file
@@ -0,0 +1,44 @@
<template>
    <view>
        <jushi-signature :settings="settings" @change="signatureChange"></jushi-signature>
        <view class="" style="margin-top: 20rpx;">
            <text class="text">保存后的签名图片</text>
            <view class="preview">
                <image :src="imgUrl" mode="" style="width: 100%;"></image>
            </view>
        </view>
    </view>
</template>
<script>
    export default {
        data() {
            return {
                settings:{ //签名设置
                    width: '750',//签名区域的宽
                    height: '500',//签名区域的高
                    lineWidth:3,//签名时线宽
                    textColor:'#007AFF' //签名文字颜色
                },
                imgUrl: ''
            }
        },
        methods: {
            signatureChange(e) {
                this.imgUrl = e
            }
        }
    }
</script>
<style>
    .preview{
        margin: 10rpx;
        border: 1rpx solid #aaaaaa;
        border-radius: 10rpx;
    }
    .text {
        margin: 20rpx;
        color: #aaaaaa;
    }
</style>
subPackage/bs/views/repair.vue
@@ -1,7 +1,7 @@
<template>
    <view class="">
        <u-navbar height="48" :autoBack="true" safeAreaInsetTop placeholder bgColor="transparent" leftIconColor="#fff">
        </u-navbar>
<!--         <u-navbar height="48" :autoBack="true" safeAreaInsetTop placeholder bgColor="transparent" leftIconColor="#fff">
        </u-navbar> -->
        <view class="top">
            <image class="top-img" src="/static/img/repair-bg.png" mode="aspectFill"></image>
        </view>
@@ -163,7 +163,7 @@
        padding: 0 30rpx;
        position: fixed;
        box-sizing: border-box;
        top: 130rpx;
        top: 40rpx;
        left: 0;
        z-index: 100;
subPackage/house/family/index.vue
@@ -1,10 +1,19 @@
<template>
    <view class="container">
        <view class="flex f-d-c main">
            <view class="cur-header">
            <view class="cur-header"  v-if="!addressName">
                <u-icon name="/static/icon/map.png" width="15" height="18"></u-icon>
                <view>{{ curSelectSite.name }}/{{housingName}}{{buildingName}}</view>
            </view>
            <block  v-if="addressName">
            <view class="cur-header">
                <u-icon name="/static/icon/map.png" width="15" height="18"></u-icon>
                <view>{{housingName}}/{{buildingName}}</view>
            </view>
            <view class="house-address f-26  bgc-ff"  >
                {{addressName}}
            </view>
            </block>
            <view class="h0 flex-1 build-list-box content">
                <u-collapse :value="buildingList.length == 1?['0']:''">
                    <u-collapse-item v-if="isShowBuild" style="border: none;" :name="index" :title="item.unitName"
@@ -21,7 +30,7 @@
                            <view class="room-content">
                                <view class="flex flex-wrap j-c-s-b" style="width: 100%;">
                                    <view class="room-box flex f-d-c" v-for="(scItem, scIndex) in cItem.children"
                                        :key="scItem.id" @click="pushPage"  :data-code="scItem.addressCode">
                                        :key="scItem.id" @click="pushPage"  :data-code="scItem.addressCode"  :data-unit="item.unitName">
                                        <view class="flex a-i-c j-c-s-b">
                                            <!-- <view class="l">
                                                <u-icon name="/static/icon/person.png" size="16"></u-icon>
@@ -135,7 +144,8 @@
                neiCode: "",
                buildingList: [],
                shopList: [],
                curSelectSite: {}
                curSelectSite: {},
                addressName:""
            }
        },
        onLoad(e) {
@@ -149,8 +159,16 @@
            this.currentId = id
            this.housingName = housingName
            this.buildingName = buildingName
            this.addressType = addressType
            if(addressType){
                    this.addressType = addressType
            }
            this.neiCode = neiCode
            if(e.address){
                this.addressName = e.address;
                this.housingName = e.neiName;
                this.buildingName = e.aoiName
            }
        },
        onShow() {
@@ -214,7 +232,8 @@
                }
            },
            pushPage(e) {
                let url = `/subPackage/house/roomDetails/index?id=${e.currentTarget.dataset.code}`
                let { code,unit } = e.currentTarget.dataset;
                let url = `/subPackage/house/roomDetails/index?id=${code}&unit=${unit}`
                this.$u.func.globalNavigator(url, "navTo")
            },
            // 跳转到商铺页面
@@ -385,4 +404,9 @@
    /deep/ .u-cell__body {
        background-color: rgb(236, 244, 255);
    }
    .house-address{
        padding:20rpx;
    }
</style>
subPackage/house/list/index.vue
@@ -4,6 +4,7 @@
            <u-icon name="/static/icon/map.png" width="15" height="18"></u-icon>
            <text>{{ curSelectSite.name }}/{{ curHouseTitle }}</text>
        </view>
        <view class="flex f-d-c main">
            <view class="flex house-container">
                <view class="house-list-box" v-if="isShowAoi" v-for="(item, index) in houseList"
@@ -59,11 +60,13 @@
            const {
                id,
                title,
                addressType
            } = e
            this.currentId = id
            this.curHouseTitle = title
            this.addressType = addressType
            if(e.addressType){
                this.addressType = e.addressType
            }
        },
        onShow() {
subPackage/house/roomDetails/index.vue
@@ -10,11 +10,15 @@
                            {{ houseInfo.houseTitle }}
                        </view>
                    </view>
                    <view class="house-address f-26"  v-if="from">
                        {{houseInfo.addressName}}
                    </view>
                    <view class="flex j-c-s-b info-content">
                        <view class="house-info">
                            <text v-if="houseInfo.unitName != null">{{ houseInfo.unitName }}单元</text>
                            {{ houseInfo.houseName }}室
                            <text>(共{{ houseInfo.allNum }}人)</text>
                            <text> {{houseInfo.buildingName}} {{houseInfo.unitName?houseInfo.unitName:"一单元"}} {{ houseInfo.houseName }}室 </text>
                            <text class="c-aa">(共{{ houseInfo.allNum }}人)</text>
                        </view>
                        <view class="flex a-i-c">
                            <u-button @click="rommManage" size='small' type="primary" class="u_btn_blue"
@@ -41,7 +45,7 @@
                                </view>
                                <view class="flex">
                                    关系:
                                    <view class="flex a-i-c">
                                    <view class="flex a-i-c"  v-if="item.roleRelationName">
                                        <u-tag :text="item.roleRelationName" size="mini"
                                            :borderColor="item.relationship == 1 ? '#F2BF42' : '#1989FA'"
                                            :bgColor="item.relationship == 1 ? '#F2BF42' : '#1989FA'"></u-tag>
@@ -161,7 +165,8 @@
                    unitName: '',
                    houseTitle: '',
                    // 人员数量
                    allNum: 0
                    allNum: 0,
                    buildingName:""
                },
                ownerInfoList: [],
                rentOutList: [],
@@ -182,18 +187,28 @@
                        value: 2
                    },
                ],
                currentTime: null
                currentTime: null,
                unitName:"",
                from:""
            }
        },
        onLoad(e) {
            if(e.id){
                const {
                    id
                    id,
                    unit
                } = e
                this.currentId = id
                this.currentId = id;
                this.unitName = unit;
            }else {
                this.currentId = uni.getStorageSync("siteInfo").houseCode
            }
            if(e.from){
                this.from = e.from;
            }
        },
        onShow() {
            this.getHouseRentInfoList()
@@ -248,7 +263,8 @@
                    unitName,
                    houseRentalList,
                    householdList,
                    subAoi
                    subAoi,
                    addressName
                } = res.data
                this.houseCode = addressCode
                this.rentOutList = houseRentalList
@@ -256,6 +272,8 @@
                this.houseInfo.houseName = houseName
                this.houseInfo.unitName = unitName
                this.houseInfo.houseTitle = aoiName || subAoi
                this.houseInfo.buildingName = buildingName
                this.houseInfo.addressName = addressName
                // 判断当前租客有没有过期
                this.rentOutList.forEach(item => {
                    // 事件格式处理
@@ -420,8 +438,7 @@
                        .house-info {
                            font-weight: 700;
                            text {
                            .c-aa{
                                color: #AAAAAA;
                            }
                        }
@@ -549,4 +566,8 @@
            border: 0;
        }
    }
    .house-address{
        padding:10rpx 0;
        border-bottom: 1rpx solid #f6f6f6;
    }
</style>
subPackage/label/index.vue
@@ -1,7 +1,7 @@
<template>
    <view class="">
        <u-navbar height="48" :autoBack="true"  safeAreaInsetTop placeholder  bgColor="transparent"  leftIconColor="#fff">
        </u-navbar>
    <!--     <u-navbar height="48" :autoBack="true"  safeAreaInsetTop placeholder  bgColor="transparent"  leftIconColor="#fff">
        </u-navbar> -->
        <view class="top">
            <image class="top-img" src="/static/img/repair-bg.png" mode="aspectFill"></image>
        </view>
@@ -181,7 +181,7 @@
        padding: 0 30rpx;
        position: fixed;
        box-sizing: border-box;
        top:130rpx;
        top:40rpx;
        left:0;
        z-index: 100;
        .serve-box{
uni_modules/jushi-signature/changelog.md
New file
@@ -0,0 +1,4 @@
## 1.0.1(2023-09-12)
增加组件属性 base64,该属性用于设置返回的结果是图片base64还是临时地址,默认为图片临时地址
## 1.0.0(2023-08-16)
手写签名、电子签名组件首次提交发布
uni_modules/jushi-signature/components/image-tools/README.md
New file
@@ -0,0 +1,76 @@
# image-tools
图像转换工具,可用于如下环境:uni-app、微信小程序、5+APP、浏览器(需允许跨域)
## 使用方式
### NPM
```
npm i image-tools --save
```
```js
import { pathToBase64, base64ToPath } from 'image-tools'
```
### 直接下载
```js
// 以下路径需根据项目实际情况填写
import { pathToBase64, base64ToPath } from '../../js/image-tools/index.js'
```
## API
### pathToBase64
从图像路径转换为base64,uni-app、微信小程序和5+APP使用的路径不支持网络路径,如果是网络路径需要先使用下载API下载下来。
```js
pathToBase64(path)
  .then(base64 => {
    console.log(base64)
  })
  .catch(error => {
    console.error(error)
  })
```
### base64ToPath
将图像base64保存为文件,返回文件路径。
```js
base64ToPath(base64)
  .then(path => {
    console.log(path)
  })
  .catch(error => {
    console.error(error)
  })
```
## 提示
可以利用promise来串行和并行的执行多个任务
```js
// 并行
Promise.all(paths.map(path => pathToBase64(path)))
  .then(res => {
    console.log(res)
    // [base64, base64...]
  })
  .catch(error => {
    console.error(error)
  })
// 串行
paths.reduce((promise, path) => promise.then(res => pathToBase64(path).then(base64 => (res.push(base64), res))), Promise.resolve([]))
  .then(res => {
    console.log(res)
    // [base64, base64...]
  })
  .catch(error => {
    console.error(error)
  })
```
uni_modules/jushi-signature/components/image-tools/index.js
New file
@@ -0,0 +1,196 @@
function getLocalFilePath(path) {
    if (path.indexOf('_www') === 0 || path.indexOf('_doc') === 0 || path.indexOf('_documents') === 0 || path.indexOf('_downloads') === 0) {
        return path
    }
    if (path.indexOf('file://') === 0) {
        return path
    }
    if (path.indexOf('/storage/emulated/0/') === 0) {
        return path
    }
    if (path.indexOf('/') === 0) {
        var localFilePath = plus.io.convertAbsoluteFileSystem(path)
        if (localFilePath !== path) {
            return localFilePath
        } else {
            path = path.substr(1)
        }
    }
    return '_www/' + path
}
function dataUrlToBase64(str) {
    var array = str.split(',')
    return array[array.length - 1]
}
var index = 0
function getNewFileId() {
    return Date.now() + String(index++)
}
function biggerThan(v1, v2) {
    var v1Array = v1.split('.')
    var v2Array = v2.split('.')
    var update = false
    for (var index = 0; index < v2Array.length; index++) {
        var diff = v1Array[index] - v2Array[index]
        if (diff !== 0) {
            update = diff > 0
            break
        }
    }
    return update
}
export function pathToBase64(path) {
    return new Promise(function(resolve, reject) {
        if (typeof window === 'object' && 'document' in window) {
            if (typeof FileReader === 'function') {
                var xhr = new XMLHttpRequest()
                xhr.open('GET', path, true)
                xhr.responseType = 'blob'
                xhr.onload = function() {
                    if (this.status === 200) {
                        let fileReader = new FileReader()
                        fileReader.onload = function(e) {
                            resolve(e.target.result)
                        }
                        fileReader.onerror = reject
                        fileReader.readAsDataURL(this.response)
                    }
                }
                xhr.onerror = reject
                xhr.send()
                return
            }
            var canvas = document.createElement('canvas')
            var c2x = canvas.getContext('2d')
            var img = new Image
            img.onload = function() {
                canvas.width = img.width
                canvas.height = img.height
                c2x.drawImage(img, 0, 0)
                resolve(canvas.toDataURL())
                canvas.height = canvas.width = 0
            }
            img.onerror = reject
            img.src = path
            return
        }
        if (typeof plus === 'object') {
            plus.io.resolveLocalFileSystemURL(getLocalFilePath(path), function(entry) {
                entry.file(function(file) {
                    var fileReader = new plus.io.FileReader()
                    fileReader.onload = function(data) {
                        resolve(data.target.result)
                    }
                    fileReader.onerror = function(error) {
                        reject(error)
                    }
                    fileReader.readAsDataURL(file)
                }, function(error) {
                    reject(error)
                })
            }, function(error) {
                reject(error)
            })
            return
        }
        if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
            wx.getFileSystemManager().readFile({
                filePath: path,
                encoding: 'base64',
                success: function(res) {
                    resolve('data:image/png;base64,' + res.data)
                },
                fail: function(error) {
                    reject(error)
                }
            })
            return
        }
        reject(new Error('not support'))
    })
}
export function base64ToPath(base64) {
    return new Promise(function(resolve, reject) {
        if (typeof window === 'object' && 'document' in window) {
            base64 = base64.split(',')
            var type = base64[0].match(/:(.*?);/)[1]
            var str = atob(base64[1])
            var n = str.length
            var array = new Uint8Array(n)
            while (n--) {
                array[n] = str.charCodeAt(n)
            }
            return resolve((window.URL || window.webkitURL).createObjectURL(new Blob([array], { type: type })))
        }
        var extName = base64.split(',')[0].match(/data\:\S+\/(\S+);/)
        if (extName) {
            extName = extName[1]
        } else {
            reject(new Error('base64 error'))
        }
        var fileName = getNewFileId() + '.' + extName
        if (typeof plus === 'object') {
            var basePath = '_doc'
            var dirPath = 'uniapp_temp'
            var filePath = basePath + '/' + dirPath + '/' + fileName
            if (!biggerThan(plus.os.name === 'Android' ? '1.9.9.80627' : '1.9.9.80472', plus.runtime.innerVersion)) {
                plus.io.resolveLocalFileSystemURL(basePath, function(entry) {
                    entry.getDirectory(dirPath, {
                        create: true,
                        exclusive: false,
                    }, function(entry) {
                        entry.getFile(fileName, {
                            create: true,
                            exclusive: false,
                        }, function(entry) {
                            entry.createWriter(function(writer) {
                                writer.onwrite = function() {
                                    resolve(filePath)
                                }
                                writer.onerror = reject
                                writer.seek(0)
                                writer.writeAsBinary(dataUrlToBase64(base64))
                            }, reject)
                        }, reject)
                    }, reject)
                }, reject)
                return
            }
            var bitmap = new plus.nativeObj.Bitmap(fileName)
            bitmap.loadBase64Data(base64, function() {
                bitmap.save(filePath, {}, function() {
                    bitmap.clear()
                    resolve(filePath)
                }, function(error) {
                    bitmap.clear()
                    reject(error)
                })
            }, function(error) {
                bitmap.clear()
                reject(error)
            })
            return
        }
        if (typeof wx === 'object' && wx.canIUse('getFileSystemManager')) {
            var filePath = wx.env.USER_DATA_PATH + '/' + fileName
            wx.getFileSystemManager().writeFile({
                filePath: filePath,
                data: dataUrlToBase64(base64),
                encoding: 'base64',
                success: function() {
                    resolve(filePath)
                },
                fail: function(error) {
                    reject(error)
                }
            })
            return
        }
        reject(new Error('not support'))
    })
}
uni_modules/jushi-signature/components/image-tools/package.json
New file
@@ -0,0 +1,25 @@
{
  "name": "image-tools",
  "version": "1.4.0",
  "description": "图像转换工具,可用于如下环境:uni-app、微信小程序、5+APP、浏览器",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/zhetengbiji/image-tools.git"
  },
  "keywords": [
    "base64"
  ],
  "author": "Shengqiang Guo",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/zhetengbiji/image-tools/issues"
  },
  "homepage": "https://github.com/zhetengbiji/image-tools#readme",
  "devDependencies": {
    "@types/html5plus": "^1.0.0"
  }
}
uni_modules/jushi-signature/components/jushi-signature/jushi-signature.vue
New file
@@ -0,0 +1,228 @@
<template>
    <view class="container">
        <view class="center" id="center">
            <text class="sign-area">签名区域</text>
            <canvas canvas-id="jushiSignature" :style="{width:`${settings.width}rpx`,height:`${settings.height}rpx`}"
                disable-scroll="true" @touchstart="touchstart" @touchmove="touchmove" @touchend="touchend"></canvas>
        </view>
        <view class="btn-view">
            <view class="save" @click="save()">保存</view>
            <view class="clear" @click="clear()">清除</view>
        </view>
    </view>
</template>
<script>
    import {
        pathToBase64,
        base64ToPath
    } from '../image-tools/index.js'
    var ctx = null
    var tempPoint = [] //存放当前画纸上的轨迹点
    export default {
        props: {
            settings: { //签名设置
                type: Object,
                default: () => {
                    return {
                        width: '750', //签名区域的宽
                        height: '500', //签名区域的高
                        lineWidth: 4, //签名时线宽
                        textColor: '#000000' //签名文字颜色
                    }
                }
            },
            base64: { //是否强制返回base64
                type: Boolean,
                default: false
            }
        },
        data() {
            return {
                points: [], //路径点
                canvasWidth: 0,
                canvasHeight: 0
            };
        },
        created() {
            //微信小程序 需传第二个参数 this才生效
            ctx = uni.createCanvasContext('jushiSignature', this)
            this.setPaintStyle()
        },
        onReady() {
            // #ifdef MP-WEIXIN
            const query = uni.createSelectorQuery().in(this);
            query.select('#center').boundingClientRect(data => {
                this.canvasWidth = data.width
                this.canvasHeight = data.height
                this.setCanvasBg()
            }).exec();
            // #endif
        },
        methods: {
            setPaintStyle() { //画笔样式
                ctx.lineWidth = this.settings.lineWidth
                ctx.lineCap = "round"
                ctx.lineJoin = "round"
                ctx.setStrokeStyle(this.settings.textColor)
            },
            touchstart(e) {
                const startX = e.changedTouches[0].x
                const startY = e.changedTouches[0].y
                let startPoint = {
                    X: startX,
                    Y: startY
                }
                this.points.push(startPoint)
                //每次触摸开始,开启新的路径
                ctx.beginPath()
            },
            touchmove(e) {
                let moveX = e.changedTouches[0].x
                let moveY = e.changedTouches[0].y
                let movePoint = {
                    X: moveX,
                    Y: moveY
                }
                this.points.push(movePoint); //存点
                if (this.points.length >= 2) {
                    this.draw() //绘制路径
                }
                tempPoint.push(movePoint)
            },
            touchend() { // 清空未绘制的点避免对后续路径产生干扰
                this.points = []
            },
            /*
             *   绘制笔迹
             *   1.移动的同时绘制笔迹,保证实时显示
             *   2.从路径中取两个点作为起点(moveTo)和终点(lineTo)保证笔迹连续性
             *   3.把上一次的终点作为下一次绘制的起点(即清除第一个点)
             * */
            draw() {
                let p1 = this.points[0]
                let p2 = this.points[1]
                this.points.shift()
                ctx.moveTo(p1.X, p1.Y)
                ctx.lineTo(p2.X, p2.Y)
                ctx.stroke()
                ctx.draw(true)
            },
            clear() { //清空画布
                let that = this
                uni.getSystemInfo({
                    success: function(res) {
                        ctx.clearRect(0, 0, res.windowWidth, res.windowHeight)
                        ctx.draw(true)
                        // #ifdef MP-WEIXIN
                        that.setCanvasBg()
                        // #endif
                        that.setPaintStyle()
                    },
                })
                tempPoint = []
                that.emit('')
            },
            save() { //保存
                let that = this
                if (tempPoint.length == 0) {
                    uni.showToast({
                        title: '您还未签名,请先签名',
                        icon: 'none',
                        duration: 2000
                    });
                    return
                }
                uni.canvasToTempFilePath({
                    canvasId: 'jushiSignature',
                    fileType: 'jpg',
                    quality: 1,
                    success: function(res) {
                        //强制返回base64
                        if (that.base64) {
                            if (res.tempFilePath.startsWith('data:image/jpeg;base64')) {
                                that.emit(res.tempFilePath)
                            } else {
                                pathToBase64(res.tempFilePath).then(e => {
                                    that.emit(e)
                                }).catch(e => {
                                    console.log(JSON.stringify(e))
                                })
                            }
                        } else {
                            if (res.tempFilePath.startsWith('data:image/jpeg;base64')) {
                                base64ToPath(res.tempFilePath).then(e => {
                                    that.emit(e)
                                }).catch(e => {
                                    console.log(JSON.stringify(e))
                                })
                            } else {
                                that.emit(res.tempFilePath)
                            }
                        }
                    },
                    fail(e) {
                        console.log(JSON.stringify(e))
                    }
                }, this)
            },
            emit(tempFilePath) {
                this.$emit('change', tempFilePath)
            },
            setCanvasBg() { //设置canvas背景色  不设置  导出的canvas的背景为黑色
                ctx.rect(0, 0, this.canvasWidth, this.canvasHeight)
                ctx.setFillStyle('#ffffff')
                ctx.fill()
                ctx.draw()
            }
        }
    };
</script>
<style lang="scss">
    .center {
        background-color: #ffffff;
        display: flex;
        flex-direction: column;
        position: relative;
    }
    .btn-view {
        margin-top: 20rpx;
        font-size: 14px;
        display: flex;
        justify-content: space-around;
        align-items: center;
    }
    .save,
    .clear {
        height: 70rpx;
        width: 200rpx;
        text-align: center;
        font-weight: bold;
        color: white;
        border-radius: 5rpx;
        align-items: center;
        justify-content: center;
        flex-direction: row;
        display: flex;
    }
    .save {
        background: #007AFF;
    }
    .clear {
        background: orange;
    }
    .sign-area {
        position: absolute;
        top: 40%;
        left: 15%;
        color: #eeeeee;
        font-size: 130rpx;
        transform: rotate(-20deg);
    }
</style>
uni_modules/jushi-signature/package.json
New file
@@ -0,0 +1,83 @@
{
  "id": "jushi-signature",
  "displayName": "手写签名、电子签名组件,支持APP、微信小程序、H5",
  "version": "1.0.1",
  "description": "手写签名、电子签名组件,支持APP、微信小程序、H5",
  "keywords": [
    "在线签名",
    "手写签名",
    "电子签名",
    "手写板"
],
  "repository": "",
"engines": {
  },
  "dcloudext": {
    "type": "component-vue",
    "sale": {
      "regular": {
        "price": "0.00"
      },
      "sourcecode": {
        "price": "0.00"
      }
    },
    "contact": {
      "qq": ""
    },
    "declaration": {
      "ads": "无",
      "data": "插件不采集任何数据",
      "permissions": "无"
    },
    "npmurl": ""
  },
  "uni_modules": {
    "dependencies": [],
    "encrypt": [],
    "platforms": {
      "cloud": {
        "tcb": "y",
        "aliyun": "y"
      },
      "client": {
        "Vue": {
          "vue2": "y",
          "vue3": "u"
        },
        "App": {
          "app-vue": "y",
          "app-nvue": "y"
        },
        "H5-mobile": {
          "Safari": "y",
          "Android Browser": "y",
          "微信浏览器(Android)": "y",
          "QQ浏览器(Android)": "y"
        },
        "H5-pc": {
          "Chrome": "u",
          "IE": "u",
          "Edge": "u",
          "Firefox": "u",
          "Safari": "u"
        },
        "小程序": {
          "微信": "y",
          "阿里": "u",
          "百度": "u",
          "字节跳动": "u",
          "QQ": "u",
          "钉钉": "u",
          "快手": "u",
          "飞书": "u",
          "京东": "u"
        },
        "快应用": {
          "华为": "u",
          "联盟": "u"
        }
      }
    }
  }
}
uni_modules/jushi-signature/readme.md
New file
@@ -0,0 +1,82 @@
#### 手写签名、电子签名组件,支持APP、微信小程序、H5
 - 支持APP、微信小程序、H5
 ```
本插件为手写签名(电子签名)组件,支持APP、微信小程序、H5三端使用
 ```
 - 组件属性说明
 | 序号 | 属性名称|属性说明
 |--|--|--|
 |1 |settings|签名设置项,签名区域宽高、文字颜色等设置
 |2 |base64|是否强制返回图片base64,默认为false,即返回的是临时地址
 - settings属性说明
 ```
 { //签名设置
     width: '750',//签名区域的宽
     height: '500',//签名区域的高
     lineWidth:3,//签名时线宽
     textColor:'#007AFF' //签名文字颜色
 }
 ```
 - 组件事件说明
 |序号|事件名称|事件说明
 |--|--|--|
 |1|change|点击保存/清除按钮事件,返回图片地址或图片base64(根据base64属性的值返回相应的结果),点击清除按钮是返回 ''
 - 使用示例
```
    <template>
        <view>
            <jushi-signature base64 :settings="settings" @change="signatureChange"></jushi-signature>
            <view class="" style="margin-top: 20rpx;">
                <text class="text">保存后的签名图片</text>
                <view class="preview">
                    <image :src="imgUrl" mode="" style="width: 100%;"></image>
                </view>
            </view>
        </view>
    </template>
    <script>
        export default {
            data() {
                return {
                    settings:{ //签名设置
                        width: '750',//签名区域的宽
                        height: '500',//签名区域的高
                        lineWidth:3,//签名时线宽
                        textColor:'#007AFF' //签名文字颜色
                    },
                    imgUrl: ''
                }
            },
            methods: {
                signatureChange(e) {
                    this.imgUrl = e
                    console.log(e)
                }
            }
        }
    </script>
    <style>
        .preview{
            margin: 10rpx;
            border: 1rpx solid #aaaaaa;
            border-radius: 10rpx;
        }
        .text {
            margin: 20rpx;
            color: #aaaaaa;
        }
    </style>
```
 - 备注
 ```
    欢迎来邮件咨询和讨论,邮箱:1052775690@qq.com
 ```