Browse Source

陪护添加服务费及签名

hzd 5 months ago
parent
commit
3996418ff9

+ 3 - 0
App.vue

@@ -4,6 +4,9 @@
 			host: 'http://wy2.jiliyilian.com/wap',
 			serverUrl: 'http://wy2.jiliyilian.com/api/h5/',
 			uploadServerUrl: 'http://wy2.jiliyilian.com/api/h5/',
+			// host: 'http://jili2.demo.com/wap',
+			// serverUrl: 'http://jili2.demo.com/api/h5/',
+			// uploadServerUrl: 'http://jili2.demo.com/api/h5/',
 			appId: 'wx439b9d9527727911',
 			redirectUri: 'http://wy2.jiliyilian.com/wap/ph/#/pages/index/wechat',
 			storagePre: 'ph_',

+ 53 - 0
components/sp-sign-board/changelog.md

@@ -0,0 +1,53 @@
+## 1.1.9(2024-08-28)
+1. 提供了弹窗模式,详见示例二
+2. 在弹窗模式下,微信小程序请遵守示例二的关键步骤,正确对签字板进行初始化与销毁
+## 1.1.8(2024-08-21)
+1. 修复h5端横屏时可能会出现签名回显的问题
+## 1.1.7(2024-07-23)
+1. 修复签字偏移问题
+## 1.1.6(2024-06-28)
+1.新增needBack属性:点击取消时是否需要自动返回(默认true)
+2.修正原取消事件cancel单词写错成cancle的问题
+## 1.1.5(2024-06-28)
+1.优化
+## 1.1.4(2024-05-10)
+1. 删除冗余代码
+2. 文档迁移
+## 1.1.3(2024-04-29)
+1. 更新示例工程
+## 1.1.2(2024-04-29)
+1. 优化了多场景下签名的需求,详情请看示例工程
+## 1.1.1(2024-04-29)
+1. 更新
+## 1.1.0(2024-04-01)
+1. 更好的兼容支付宝小程序
+## 1.0.9(2024-03-20)
+1. 修复小程序无法横屏的问题
+## 1.0.8(2024-02-28)
+1. 修复背景颜色修改无效的问题
+## 1.0.7(2024-02-04)
+1. 修复签名板在h5端高度异常问题
+2. 更新示例工程
+3. 更新文档
+## 1.0.6(2023-10-20)
+1. 新增@firstTouchStart首次触碰绘制的方法
+2. 修复添加动态水印时,初次加载水印失效的bug
+3. 更新示例项目中水印的使用方式
+## 1.0.5(2023-10-17)
+1. 更新示例项目
+## 1.0.4(2023-10-17)
+1. 更新文档说明
+## 1.0.3(2023-10-17)
+1. 新增图片导出格式与画质配置
+2. 更新条件编译以更好适配H5、App和小程序端
+## 1.0.2(2023-10-16)
+1. 修复横屏适配问题
+2. 文档更新
+## 1.0.1(2023-10-16)
+1. 修复安卓真机canvas无法动态设置宽高的bug
+2. 修复安卓真机水印与背景底色失效的bug
+3. 新增监听path字段,现在不仅可以返回base64图片,也可返回临时的图片路径
+4. 修复vue2、vue3兼容性
+## 1.0.0(2023-10-13)
+1. 签字板可添加水印,背景图片,调整画笔样式等
+2. 适配横竖屏,生成base64格式图片

+ 196 - 0
components/sp-sign-board/components/sp-sign-board/index.js

@@ -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'))
+    })
+}

+ 476 - 0
components/sp-sign-board/components/sp-sign-board/sp-sign-board.vue

@@ -0,0 +1,476 @@
+<template>
+  <view class="sign-page">
+    <view class="sign-body">
+      <canvas
+        id="signCanvas"
+        canvas-id="signCanvas"
+        class="sign-canvas"
+        disable-scroll
+        @touchstart="signCanvasStart"
+        @touchmove="signCanvasMove"
+        @touchend="signCanvasEnd"
+      ></canvas>
+      <!-- #ifndef APP -->
+      <!--用于临时储存横屏图片的canvas容器,H5和小程序需要-->
+      <canvas
+        v-if="horizontal"
+        id="hsignCanvas"
+        canvas-id="hsignCanvas"
+        style="position: absolute; left: -1000px; z-index: -1"
+        :style="{ width: canvasHeight + 'px', height: canvasWidth + 'px' }"
+      ></canvas>
+      <!-- #endif -->
+    </view>
+    <view v-if="!popupMode" class="sign-footer" :class="[horizontal ? 'horizontal-btns' : 'vertical-btns']">
+      <view class="btn" @click="cancel">取消</view>
+      <view class="btn" @click="reset">重写</view>
+      <view class="btn" @click="confirm">确认</view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { pathToBase64, base64ToPath } from './index.js'
+export default {
+  name: 'sign',
+  props: {
+    // 签字板id,用于多签名场景下作为区分
+    sid: {
+      type: String,
+      default: 'sign-board'
+    },
+    // 是否是弹窗模式
+    popupMode: {
+      type: Boolean,
+      default: false
+    },
+    // 背景水印图,优先级大于 bgColor
+    bgImg: {
+      type: String,
+      default: ''
+    },
+    // 背景纯色底色,为空则透明
+    bgColor: {
+      type: String,
+      default: ''
+    },
+    // 是否显示水印
+    showMark: {
+      type: Boolean,
+      default: true
+    },
+    // 水印内容,可多行
+    markText: {
+      type: Array,
+      default: () => {
+        return [] // ['水印1', '水印2']
+      }
+    },
+    // 水印样式
+    markStyle: {
+      type: Object,
+      default: () => {
+        return {
+          fontSize: 12, // 水印字体大小
+          fontFamily: 'microsoft yahei', // 水印字体
+          color: '#cccccc', // 水印字体颜色
+          rotate: 60, // 水印旋转角度
+          step: 2.2 // 步长,部分场景下可通过调节该参数来调整水印间距,建议为1.4-2.6左右
+        }
+      }
+    },
+    // 是否横屏
+    horizontal: {
+      type: Boolean,
+      default: false
+    },
+    // 画笔样式
+    penStyle: {
+      type: Object,
+      default: () => {
+        return {
+          lineWidth: 3, // 画笔线宽 建议1~5
+          color: '#000000' // 画笔颜色
+        }
+      }
+    },
+    // 导出图片配置
+    expFile: {
+      type: Object,
+      default: () => {
+        return {
+          fileType: 'png', // png/jpg (png不可压缩质量,支持透明;jpg可压缩质量,不支持透明)
+          quality: 1 // 范围 0 - 1 (仅jpg支持)
+        }
+      }
+    },
+    // 取消时是否需要返回
+    needBack: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data() {
+    return {
+      canvasCtx: null, // canvascanvasWidth: 0, // canvas宽度
+      canvasWidth: 0, // canvas宽度
+      canvasHeight: 0, // canvas高度
+      x0: 0, // 初始横坐标或上一段touchmove事件中触摸点的横坐标
+      y0: 0, // 初始纵坐标或上一段touchmove事件中触摸点的纵坐标
+      signFlag: false // 签名旗帜
+    }
+  },
+  computed: {},
+  created() {},
+  mounted() {
+    this.$nextTick(() => {
+      this.createCanvas()
+    })
+  },
+  methods: {
+    // 创建canvas实例
+    createCanvas() {
+      this.canvasCtx = uni.createCanvasContext('signCanvas', this)
+      this.canvasCtx.setLineCap('round') // 向线条的每个末端添加圆形线帽
+
+      // 获取canvas宽高
+      const query = uni.createSelectorQuery().in(this)
+      query
+        .select('.sign-body')
+        .boundingClientRect((data) => {
+          this.canvasWidth = data.width
+          this.canvasHeight = data.height
+        })
+        .exec(async () => {
+          await this.drawBg()
+          this.drawMark(this.markText)
+        })
+    },
+    async drawBg() {
+      if (this.bgImg) {
+        const img = await uni.getImageInfo({ src: this.bgImg })
+        this.canvasCtx.drawImage(img.path, 0, 0, this.canvasWidth, this.canvasHeight)
+      } else if (this.bgColor) {
+        // 绘制底色填充,否则为透明
+        this.canvasCtx.setFillStyle(this.bgColor)
+        this.canvasCtx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
+      }
+    },
+    // 绘制动态水印
+    drawMark(textArray) {
+      if (!this.showMark) {
+        this.canvasCtx.draw()
+        return
+      }
+      // 绘制背景
+      this.drawBg()
+
+      // 水印参数
+      const markStyle = Object.assign(
+        {
+          fontSize: 12, // 水印字体大小
+          fontFamily: 'microsoft yahei', // 水印字体
+          color: '#cccccc', // 水印字体颜色
+          rotate: 60, // 水印旋转角度
+          step: 2 // 步长,部分场景下可通过调节该参数来调整水印间距,建议为1.4-2.6左右
+        },
+        this.markStyle
+      )
+      this.canvasCtx.font = `${markStyle.fontSize}px ${markStyle.fontFamily}`
+      this.canvasCtx.fillStyle = markStyle.color
+      // 文字坐标
+      const maxPx = Math.max(this.canvasWidth / 2, this.canvasHeight / 2)
+      const stepPx = Math.floor(maxPx / markStyle.step)
+      let arrayX = [0] // 初始水印位置 canvas坐标 0 0 点
+      while (arrayX[arrayX.length - 1] < maxPx / 2) {
+        arrayX.push(arrayX[arrayX.length - 1] + stepPx)
+      }
+      arrayX.push(
+        ...arrayX.slice(1, arrayX.length).map((item) => {
+          return -item
+        })
+      )
+
+      for (let i = 0; i < arrayX.length; i++) {
+        for (let j = 0; j < arrayX.length; j++) {
+          this.canvasCtx.save()
+          this.canvasCtx.translate(this.canvasWidth / 2, this.canvasHeight / 2) // 画布旋转原点 移到 图片中心
+          this.canvasCtx.rotate(Math.PI * (markStyle.rotate / 180))
+          textArray.forEach((item, index) => {
+            let offsetY = markStyle.fontSize * index
+            this.canvasCtx.fillText(item, arrayX[i], arrayX[j] + offsetY)
+          })
+          this.canvasCtx.restore()
+        }
+      }
+
+      this.canvasCtx.draw()
+    },
+    cancel() {
+      //取消按钮事件
+      this.$emit('cancel')
+      this.reset()
+	  console.log("cancel");
+      // if (this.needBack) uni.navigateBack()
+    },
+    async reset() {
+      this.$emit('reset')
+      this.signFlag = false
+      this.canvasCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
+      await this.drawBg()
+      this.drawMark(this.markText)
+    },
+    async confirm() {
+		// console.log("confirm");
+      this.$emit('confirm')
+      // 确认按钮事件
+      if (!this.signFlag) {
+		  // console.log("请签名后再点击确定");
+        // uni.showToast({
+        //   title: '请签名后再点击确定',
+        //   icon: 'none',
+        //   duration: 2000
+        // })
+        return
+      }
+// console.log("确认签名无误吗");
+let tempFile
+            if (this.horizontal) {
+              tempFile = await this.saveHorizontalCanvas()
+            } else {
+              tempFile = await this.saveCanvas()
+            }
+            const base64 = await pathToBase64(tempFile)
+            const path = await base64ToPath(base64)
+            uni.$emit('getSignImg', { base64, path, sid: this.sid })
+      // uni.showModal({
+      //   title: '确认',
+      //   content: '确认签名无误吗',
+      //   showCancel: true,
+      //   success: async ({ confirm }) => {
+      //     if (confirm) {
+      //       let tempFile
+      //       if (this.horizontal) {
+      //         tempFile = await this.saveHorizontalCanvas()
+      //       } else {
+      //         tempFile = await this.saveCanvas()
+      //       }
+      //       const base64 = await pathToBase64(tempFile)
+      //       const path = await base64ToPath(base64)
+      //       uni.$emit('getSignImg', { base64, path, sid: this.sid })
+      //       if (this.needBack) uni.navigateBack()
+      //     }
+      //   }
+      // })
+    },
+    signCanvasEnd(e) {
+      // 签名抬起事件
+      // console.log(e, 'signCanvasEnd')
+      this.x0 = 0
+      this.y0 = 0
+    },
+    signCanvasMove(e) {
+      // 签名滑动事件
+      // console.log(e, 'signCanvasMove')
+      let dx = e.touches[0].x
+      let dy = e.touches[0].y
+
+      this.canvasCtx.moveTo(this.x0, this.y0)
+      this.canvasCtx.lineTo(dx, dy)
+      this.canvasCtx.setLineWidth(this.penStyle?.lineWidth || 4)
+      this.canvasCtx.strokeStyle = this.penStyle?.color || '#000000' // 赋值过去
+      this.canvasCtx.stroke()
+      this.canvasCtx.draw(true)
+
+      this.x0 = e.touches[0].x
+      this.y0 = e.touches[0].y
+    },
+    signCanvasStart(e) {
+      // 签名按下事件 app获取的e不一样区分小程序app
+      // console.log('signCanvasStart', e)
+      if (!this.signFlag) {
+        // 第一次开始触碰事件
+        this.$emit('firstTouchStart')
+      }
+      this.signFlag = true
+      this.x0 = e.touches[0].x
+      this.y0 = e.touches[0].y
+    },
+    // 保存竖屏图片
+    async saveCanvas() {
+      return await new Promise((resolve, reject) => {
+        uni.canvasToTempFilePath(
+          {
+            canvasId: 'signCanvas',
+            fileType: this.expFile.fileType, // 只支持png和jpg
+            quality: this.expFile.quality, // 范围 0 - 1
+            success: (res) => {
+              if (!res.tempFilePath) {
+                uni.showModal({
+                  title: '提示',
+                  content: '保存签名失败',
+                  showCancel: false
+                })
+                return
+              }
+              resolve(res.tempFilePath)
+            },
+            fail: (r) => {
+              console.log('图片生成失败:' + r)
+              resolve(false)
+            }
+          },
+          this
+        )
+      })
+    },
+    // 保存横屏图片
+    async saveHorizontalCanvas() {
+      return await new Promise((resolve, reject) => {
+        uni.canvasToTempFilePath(
+          {
+            canvasId: 'signCanvas',
+            fileType: this.expFile.fileType, // 只支持png和jpg
+            success: (res) => {
+              if (!res.tempFilePath) {
+                uni.showModal({
+                  title: '提示',
+                  content: '保存签名失败',
+                  showCancel: false
+                })
+                return
+              }
+
+              // #ifdef APP
+              uni.compressImage({
+                src: res.tempFilePath,
+                quality: this.expFile.quality * 100, // 范围 0 - 100
+                rotate: 270,
+                success: (r) => {
+                  console.log('==== compressImage :', r)
+                  resolve(r.tempFilePath)
+                }
+              })
+              // #endif
+
+              // #ifndef APP
+              uni.getImageInfo({
+                src: res.tempFilePath,
+                success: (r) => {
+                  // console.log('==== getImageInfo :', r)
+                  // 将signCanvas的内容复制到hsignCanvas中
+                  const hcanvasCtx = uni.createCanvasContext('hsignCanvas', this)
+                  // 横屏宽高互换
+                  hcanvasCtx.translate(this.canvasHeight / 2, this.canvasWidth / 2)
+                  hcanvasCtx.rotate(Math.PI * (-90 / 180))
+                  hcanvasCtx.drawImage(
+                    r.path,
+                    -this.canvasWidth / 2,
+                    -this.canvasHeight / 2,
+                    this.canvasWidth,
+                    this.canvasHeight
+                  )
+                  hcanvasCtx.draw(false, async () => {
+                    const hpathRes = await uni.canvasToTempFilePath(
+                      {
+                        canvasId: 'hsignCanvas',
+                        fileType: this.expFile.fileType, // 只支持png和jpg
+                        quality: this.expFile.quality // 范围 0 - 1
+                      },
+                      this
+                    )
+                    let tempFile = ''
+                    if (Array.isArray(hpathRes)) {
+                      hpathRes.some((item) => {
+                        if (item) {
+                          tempFile = item.tempFilePath
+                          return
+                        }
+                      })
+                    } else {
+                      tempFile = hpathRes.tempFilePath
+                    }
+                    resolve(tempFile)
+                  })
+                }
+              })
+              // #endif
+            },
+            fail: (err) => {
+              console.log('图片生成失败:' + err)
+              resolve(false)
+            }
+          },
+          this
+        )
+      })
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.sign-page {
+  height: 100%;
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+
+  .sign-body {
+    width: 100%;
+    flex-grow: 1;
+
+    .sign-canvas {
+      width: 100%;
+      height: 100%;
+    }
+  }
+
+  .sign-footer {
+    width: 100%;
+    height: 88rpx;
+    display: flex;
+    justify-content: space-evenly;
+    align-items: center;
+    border-top: 1px solid #cccccc;
+    box-sizing: border-box;
+
+    .btn {
+      line-height: 66rpx;
+      text-align: center;
+      border-radius: 12rpx;
+
+      &:nth-child(1) {
+        background-color: #ff0800;
+        color: #ffffff;
+      }
+
+      &:nth-child(2) {
+        background-color: #00d000;
+        color: #ffffff;
+      }
+
+      &:nth-child(3) {
+        background-color: #0184ff;
+        color: #ffffff;
+      }
+    }
+  }
+
+  .vertical-btns {
+    .btn {
+      width: 120rpx;
+      height: 66rpx;
+    }
+  }
+
+  .horizontal-btns {
+    .btn {
+      width: 66rpx;
+      height: 120rpx;
+      writing-mode: vertical-lr;
+      transform: rotate(90deg);
+    }
+  }
+}
+</style>

+ 85 - 0
components/sp-sign-board/package.json

@@ -0,0 +1,85 @@
+{
+	"id": "sp-sign-board",
+	"displayName": "签字板 签名版 手写 动态水印 可叠加背景 可导出图片",
+	"version": "1.1.9",
+	"description": "签字板,可添加背景图或动态水印,可适配横竖屏,并将签名生成base64格式图片",
+	"keywords": [
+        "签字",
+        "签名",
+        "水印",
+        "base64"
+    ],
+	"repository": "",
+	"engines": {
+		"HBuilderX": "^3.5.0"
+	},
+	"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",
+                "alipay": "n"
+			},
+			"client": {
+				"Vue": {
+					"vue2": "y",
+					"vue3": "y"
+				},
+				"App": {
+					"app-vue": "y",
+					"app-nvue": "y"
+				},
+				"H5-mobile": {
+					"Safari": "y",
+					"Android Browser": "y",
+					"微信浏览器(Android)": "y",
+					"QQ浏览器(Android)": "y"
+				},
+				"H5-pc": {
+					"Chrome": "y",
+					"IE": "y",
+					"Edge": "y",
+					"Firefox": "y",
+					"Safari": "y"
+				},
+				"小程序": {
+					"微信": "y",
+					"阿里": "y",
+					"百度": "u",
+					"字节跳动": "u",
+					"QQ": "u",
+					"钉钉": "u",
+					"快手": "u",
+					"飞书": "u",
+					"京东": "u"
+				},
+				"快应用": {
+					"华为": "u",
+					"联盟": "u"
+				}
+			}
+		}
+	}
+}

+ 11 - 0
components/sp-sign-board/readme.md

@@ -0,0 +1,11 @@
+# sp-sign-board
+
+### 文档迁移
+
+> 防止文档失效,提供下列五个地址,内容一致
+
+- [地址一](https://sonvee.github.io/sv-app-docs/docs-github/src/plugins/sp-sign-board/sp-sign-board.html)
+- [地址二](https://sv-app-docs.pages.dev/src/plugins/sp-sign-board/sp-sign-board.html)
+- [地址三](https://sv-app-docs.4everland.app/src/plugins/sp-sign-board/sp-sign-board.html)
+- [地址四](https://sv-app-docs.vercel.app/src/plugins/sp-sign-board/sp-sign-board.html) (需要梯子)
+- [地址五](https://static-mp-74bfcbac-6ba6-4f39-8513-8831390ff75a.next.bspapp.com/docs-uni/src/plugins/sp-sign-board/sp-sign-board.html) (有IP限制)

BIN
images/cuowu.png


+ 1 - 1
manifest.json

@@ -82,7 +82,7 @@
             "base" : "/wap/ph/"
         },
         "devServer" : {
-            "https" : true,
+            "https" : false,
             "port" : 80
         },
         "optimization" : {

+ 9 - 0
pages.json

@@ -35,6 +35,15 @@
 			}
 		},
 		{
+			"path": "pages/index/fail",
+			"style": {
+				"navigationBarTitleText": "下单失败",
+				"app-plus": {
+					"titleNView": false
+				}
+			}
+		},
+		{
 			"path": "pages/order/index",
 			"style": {
 				"navigationBarTitleText": "我的订单",

+ 287 - 6
pages/index/add.vue

@@ -21,6 +21,15 @@
 			    <input type="tel" v-model="phone" placeholder="请填写" />
 			  </view>
 			</view>
+			
+			<view class="form-box">
+			  <view class="form-box-label">选择服务 <text class="text-red">*</text></view>
+			  <view class="form-box-content" @click="selectCate">
+			    <view class="form-box-content-text" v-if="!cate.title">请选择</view>
+				<view class="form-box-content-text" v-if="cate.title">{{cate.title}}</view>
+			    <image src="../../images/xiangyou-xiao.png"></image>
+			  </view>
+			</view>
 		    
 		    <view class="form-box form-box2">
 		      <view class="form-box-label">被陪护人信息</view>
@@ -77,6 +86,31 @@
 		
 		    <view class="book-btn" @click="saveBtn()">提交</view>
 		  </view>
+		  
+		  <view class="protocol-box" v-if="showProtocol">
+			  <view v-html="protocol"></view>
+			  <view class="protocol-btn" @click="qrProtocolBtn()">同 意</view>
+		  </view>
+		  
+			<view class="sign-box" v-if="showSign">
+				<spsignboard
+				  ref="signBoardRef"
+				  sid="sign-board"
+				  bgColor="#ffffff"
+				  :showMark="false"
+				  :mark-text="markText"
+				  :horizontal="false"
+				  :penStyle="{ lineWidth: 4, color: '#000000' }"
+				  :expFile="{ fileType: 'jpg', quality: 0.7 }"
+				  @cancel="onCancel()"
+				  @confirm="onConfirmBoard()"
+				  @reset="reset"
+				  @firstTouchStart="firstTouchStart"
+				></spsignboard>
+		      
+			  <!-- <view class="sign-up-btn">上传签名</view> -->
+			</view>
+		  
 		  <cpicker v-if="genders.length > 0" :list="genders" :show.sync="genderShow" :did="gender.id" @confirm="selectedGenderVal"></cpicker>
 		  <cpicker v-if="deps.length > 0" :list="deps" :show.sync="depShow" :did="dep.id" @confirm="selectedDepVal"></cpicker>
 			<w-picker
@@ -92,6 +126,7 @@
 				:disabled-after="false"
 				ref="date" 
 			></w-picker>
+			<cpicker v-if="cates.length > 0" :list="cates" :show.sync="cateShow" :did="cate.id" @confirm="selectedCateVal"></cpicker>
 	</view>
 </template>
 
@@ -99,13 +134,24 @@
 	var app = getApp();
 	import cpicker from "../../components/cpicker/cpicker.vue";
 	import wpicker from "../../components/w-picker/w-picker.vue";
+	import spsignboard from "../../components/sp-sign-board/components/sp-sign-board/sp-sign-board.vue";
 	export default {
 		components: {
 			cpicker,
-			wpicker
+			wpicker,
+			spsignboard
 		},
 		data() {
 			return {
+				signBase64: "",
+				signTempimg: "",
+				markText: "陪护",
+				showProtocol: false,
+				isProtocol: false,
+				protocol: "", // 协议内容
+				showSign: false,
+				isSign: false,
+				signPath: "", // 签名上传路径
 				flag: false,
 				start:'',
 				startVisible:false,
@@ -127,10 +173,18 @@
 				},
 				depShow: false,
 				deps:[],
-				remark: ''
+				remark: '',
+				cate: {
+					id: 0,
+					title: ''
+				},
+				cates:[],
+				cateShow: false,
+				wx: null,
 			}
 		},
 		onLoad(option) {
+			var that = this;
 			var orgId = this.getUrlCode('orgId');
 			if(orgId){
 				uni.setStorageSync(app.globalData.storagePre+'orgId',orgId);
@@ -147,6 +201,16 @@
 					})
 				}
 			}
+			
+			if (this.$wechat && this.$wechat.isWechat()) {//获取定位经纬度
+				this.$wechat.getWx(function (res) {
+					console.log(res)
+					that.wx = res;
+				});
+			}
+			
+			// 生成水印内容
+			this.refreshMark()
 		},
 		onShow() {
 			var that = this;
@@ -155,14 +219,84 @@
 				console.log(res);
 				if(apiname == 'dep'){
 					that.deps = res.data.data;
+				}else if(apiname == 'cates'){
+					that.cates = res.data.data;
+				}else if(apiname == 'protocol'){
+					that.protocol = res.data.data.content;
 				} else if(apiname == 'order'){
-					uni.navigateTo({
-					    url: '/pages/index/success'
-					});
+					var payId = res.data.data.payId;
+					if(payId <= 0){
+						uni.navigateTo({
+						    url: '/pages/index/success'
+						});
+					}else{
+						that.getPayParam(payId);
+					}
+				}else if(apiname == 'pay'){
+					if(that.wx){
+						that.wx.chooseWXPay({
+							timestamp: res.data.data.timestamp,
+							nonceStr: res.data.data.nonceStr,
+							package: res.data.data.package,
+							signType: res.data.data.signType,
+							paySign: res.data.data.paySign,
+							success: function (r) {
+								// 支付成功后的回调函数
+								if (r.errMsg == "chooseWXPay:ok") {
+									uni.navigateTo({
+									    url: '/pages/index/success'
+									});
+								} else {
+									uni.navigateTo({
+									    url: '/pages/index/fail'
+									});
+								}
+							},
+							cancel: function(r) {},
+							fail:function(r){
+								// uni.showToast({
+								// 	title: '支付失败',
+								// 	icon: 'none',
+								// 	duration: 2000
+								// })
+								uni.navigateTo({
+								    url: '/pages/index/fail'
+								});
+							}
+						});
+					}else{
+						// uni.showToast({
+						// 	title: '支付调用失败,请刷新重试',
+						// 	icon: 'none',
+						// 	duration: 2000
+						// })
+						uni.navigateTo({
+						    url: '/pages/index/fail'
+						});
+					}
 				}
 			}
 			
 			this.getDep();
+			this.getCate();
+			this.getProtocol();
+			
+			// 监听一次
+			uni.$on('getSignImg', (e) => {
+			  console.log('getSignImg', e)
+			  // 多签名场景下可根据 sid 区分不同签名
+			  if (e.sid == 'sign-board') {
+			    that.signBase64 = e.base64
+			    that.signTempimg = e.path
+				that.isSign = true;
+				this.showSign = false;
+				this.saveBtn();
+				
+			  }
+			  // 一定注意不能确认签字完成后立马关闭弹窗,否则签字板会销毁,无法获取签名。需要等签名正确获取到之后再关闭弹窗
+			  // this.close()
+			})
+		
 		},
 		methods: {
 			getUrlCode (name) {
@@ -182,6 +316,20 @@
 				console.log(obj)
 				this.dep = obj;
 			},
+			selectCate(){
+				this.cateShow = true;
+			},
+			selectedCateVal(obj){
+				console.log(obj)
+				this.cate = obj;
+			},
+			getProtocol(){
+				app.ajax({
+					url: app.globalData.serverUrl + 'common/phprotocol',
+					type: 'POST',
+					apiname: 'protocol',
+				});
+			},
 			getDep(){
 				app.ajax({
 					url: app.globalData.serverUrl + 'common/dep',
@@ -189,13 +337,66 @@
 					apiname: 'dep',
 				});
 			},
+			getCate(){
+				app.ajax({
+					url: app.globalData.serverUrl + 'Worker/cates',
+					type: 'POST',
+					apiname: 'cates',
+				});
+			},
+			getPayParam(payId){
+				app.ajax({
+					url: app.globalData.serverUrl + 'PhOrders/pay',
+					type: 'POST',
+					apiname: 'pay',
+					data: {
+						payId: payId
+					}
+				});
+			},
 			onConfirm(e,type){
 				this.start = e.result;
 			},
+			onCancel(){
+				console.log("onCancel");
+				this.showSign = false;
+			},
+			onConfirmBoard(){
+				console.log("onConfirmBoard");
+				this.showSign = false;
+				this.saveBtn();
+			},
 			selectStart(){
 				this.startVisible = true;
 			},
 			
+			qrProtocolBtn(){
+				this.showProtocol = false;
+				this.isProtocol = true;
+				this.saveBtn();
+			},
+			
+			refreshMark() {
+			  // const currentDate = new Date()
+			  // const year = currentDate.getFullYear()
+			  // const month = String(currentDate.getMonth() + 1).padStart(2, '0')
+			  // const day = String(currentDate.getDate()).padStart(2, '0')
+			  // const hours = String(currentDate.getHours()).padStart(2, '0')
+			  // const minutes = String(currentDate.getMinutes()).padStart(2, '0')
+			  // const seconds = String(currentDate.getSeconds()).padStart(2, '0')
+			
+			  this.markText = ["陪护"];
+			},
+			firstTouchStart() {
+			  // 在第一次开始触碰时,更新一下时间水印,防止滞留时间太长造成时间误差(非必要)
+			  this.refreshMark()
+			  // 手动调用组件内绘制水印方法重新绘制
+			  this.$refs.signBoardRef.drawMark(this.markText)
+			},
+			reset() {
+			  this.refreshMark()
+			},
+			
 			saveBtn(){
 				if(!this.contact){
 					uni.showToast({
@@ -213,6 +414,14 @@
 					})
 					return;
 				}
+				if(!this.cate || this.cate.id <= 0){
+					uni.showToast({
+						title: '请输入选择服务',
+						icon: 'none',
+						duration: 2000
+					})
+					return;
+				}
 				if(!this.name){
 					uni.showToast({
 						title: '请输入姓名',
@@ -253,6 +462,14 @@
 					})
 					return;
 				}
+				if(!this.isProtocol){
+					this.showProtocol = true;
+					return false;
+				}
+				if(!this.isSign || !this.signBase64){
+					this.showSign = true;
+					return false;
+				}
 				let param = {
 					contact: this.contact,
 					phone: this.phone,
@@ -263,7 +480,9 @@
 					ill: this.ill,
 					start: this.start,
 					remark: this.remark,
-					depId: this.dep.id
+					depId: this.dep.id,
+					cateId: this.cate.id,
+					sign: this.signBase64
 				}
 				app.ajax({
 					url: app.globalData.serverUrl + 'worker/order',
@@ -305,4 +524,66 @@
 	    text-align: center;
 	    border-radius: 10rpx;
 	}
+	.protocol-box{
+		position: fixed;
+		z-index: 10000;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background-color: #ffffff;
+		padding: 20rpx;
+		overflow: auto;
+	}
+	
+	.protocol-btn{
+		position: fixed;
+		z-index: 10001;
+		left: 20rpx;
+		right: 20rpx;
+		bottom: 10rpx;
+		background-color: var(--themeColor);
+		height: 90rpx;
+		line-height: 90rpx;
+		color: #ffffff;
+		font-size: 34rpx;
+		font-weight: bold;
+		text-align: center;
+		border-radius: 10rpx;
+	}
+	
+	.sign-box{
+		position: fixed;
+		z-index: 100000;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		background-color: #ffffff;
+		padding: 20rpx;
+	}
+	.sign-canvas{
+		width: 750rpx;
+		height: 400rpx;
+	}
+	.sign-up-btn{
+		position: fixed;
+		z-index: 100001;
+		left: 20rpx;
+		right: 20rpx;
+		bottom: 10rpx;
+		background-color: var(--themeColor);
+		height: 90rpx;
+		line-height: 90rpx;
+		color: #ffffff;
+		font-size: 34rpx;
+		font-weight: bold;
+		text-align: center;
+		border-radius: 10rpx;
+	}
+	
+	.protocol-box image{
+		max-width: 100%;
+	}
+	
 </style>

+ 79 - 0
pages/index/fail.vue

@@ -0,0 +1,79 @@
+<template>
+	<view>
+		<image class="navbg" src="../../images/duihao-bg.png"></image>
+		<view class="nav-box">
+			<image class="nav-box-duihao" src="../../images/cuowu.png"></image>
+			<view class="nav-box-title">下单失败</view>
+			<view class="nav-box-desc">下单失败,请返回首页重新下单</view>
+		</view>
+		<view class="order-btn" @click="goIndex">返回首页</view>
+	</view>
+</template>
+
+<script>
+	var app = getApp();
+	export default {
+		data() {
+			return {
+				
+			}
+		},
+		onLoad() {
+			
+		},
+		onShow() {
+			
+		},
+		methods: {
+			goIndex(){
+				uni.reLaunch({ //关闭所有页面,跳转到闪屏页
+					url: '/pages/index/index'
+				})
+			},
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.navbg{
+	    width: 100%;
+	    height: 374rpx;
+	}
+	.nav-box{
+	    text-align: center;
+	    position: absolute;
+	    z-index: 10;
+	    width: 100%;
+	    height: 240rpx;
+	    top: 130rpx;
+	    left: 0;
+	    color: #333333;
+	}
+	.nav-box-duihao{
+	    width: 100rpx;
+	    height: 100rpx;
+	}
+	.nav-box-title{
+	    line-height: 75rpx;
+	    font-size: 36rpx;
+	    font-weight: 500;
+	}
+	.nav-box-desc{
+	    font-size: 24rpx;
+	    font-weight: 400;
+	}
+	
+	.order-btn{
+	    width: 238rpx;
+	    height: 70rpx;
+	    line-height: 70rpx;
+	    border-radius: 35rpx;
+	    border: 1rpx solid #B3B3B3;
+	    text-align: center;
+	    color: #808080;
+	    font-size: 30rpx;
+	    margin: 0 auto;
+	    margin-top: 20rpx;
+	}
+	
+</style>

+ 4 - 3
pages/index/index.vue

@@ -144,9 +144,10 @@
 				}
 			},
 			goAdd(){
-				uni.navigateTo({
-				    url: '/pages/index/add'
-				});
+				window.location.href = app.globalData.host+'/ph/#/pages/index/add';
+				// uni.navigateTo({
+				//     url: '/pages/index/add'
+				// });
 			},
 			goWorker(){
 				uni.navigateTo({

+ 17 - 0
pages/order/detail.vue

@@ -74,6 +74,10 @@
 					<view class="order-box-list-title">订单金额</view>
 					<view class="order-box-list-val">{{info.amount}}</view>
 				</view>
+				<view class="order-box-list">
+					<view class="order-box-list-title">服务费</view>
+					<view class="order-box-list-val">{{info.serviceMoney}}</view>
+				</view>
 				<view class="order-box-list2">
 					<view class="order-box-list-title">备注</view>
 					<view class="order-box-list-val">{{info.remark?info.remark:'无'}}</view>
@@ -106,6 +110,19 @@
 				</view>
 			</view>
 			
+			<view v-if="info.pays2.length > 0">
+				<view>服务费信息</view>
+				<view v-for="(item,index) in info.pays2" class="order-box3">
+					<view>金额:¥{{item.money}}</view>
+					<view>退款金额:¥{{item.money2}}</view>
+					<view>支付方式:
+					<span v-if="item.type === 2">线上</span>
+					<span v-if="item.type === 1">线下</span>
+					</view>
+					<view>日期:{{item.payTime}}</view>
+				</view>
+			</view>
+			
 			<view v-if="info.pays.length > 0">
 				<view>预收金信息</view>
 				<view v-for="(item,index) in info.pays" class="order-box3">