瀏覽代碼

手写签名

刘洋 1 年之前
父節點
當前提交
9b1b044132

+ 41 - 0
src/components/low-code/SIGN.vue

@@ -0,0 +1,41 @@
+<template>
+  <div class="sign">
+    <u-button size="mini" type="primary" @click="toWright" v-if="!imgUrl">添加签名</u-button>
+    <u-image v-else width="200rpx" mode="widthFix" :src="imgUrl" @click="toWright" class="img"></u-image>
+  </div>
+</template>
+
+<script>
+  export default {
+    name: 'SIGN',
+    props: ['config'],
+    data() {
+      return {
+        imgUrl: '',
+        id: ''
+      }
+    },
+    created() {
+      console.log('ccc', this.config)
+      this.id = this.config.id
+      uni.$on('getSignImgOssUrl', (data) => {
+        console.log('dddd', data)
+        if (data?.id == this.id) {
+          this.$u.toast('签名获取成功')
+          this.imgUrl = data.imgUrl
+          this.$emit('change', { prop: this.config.formName, value: this.imgUrl })
+        }
+      })
+    },
+    methods: {
+      toWright() {
+        this.$Router.push({ path: '/pages/sign/sign', query: { id: this.id } })
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .sign {
+  }
+</style>

+ 5 - 3
src/components/low-code/my-form-item.vue

@@ -5,7 +5,7 @@
         <text class="red" v-if="!isBigTitle && config.required">*</text>
         <text>{{ config.title }}</text>
       </view>
-      <TEXT v-if="config.code === 'TEXT'" :config="config" @change="change" type="text"></TEXT>
+      <!-- <TEXT v-if="config.code === 'TEXT'" :config="config" @change="change" type="text"></TEXT>
       <SELECT v-if="config.code === 'SELECT'" :config="config" @change="change"></SELECT>
       <MULTIPLESELECT v-if="config.code === 'MULTIPLE_SELECT'" :config="config" @change="change"></MULTIPLESELECT>
       <DATE v-if="config.code === 'DATE'" :config="config" @change="change"></DATE>
@@ -17,7 +17,8 @@
       <RADIOWITHINPUT v-if="config.code === 'RADIO_WITH_INPUT'" :config="config" @change="change"></RADIOWITHINPUT>
       <DEVICEINTABLE v-if="config.code === 'DEVICE_IN_TABLE'" :config="config" @change="change"></DEVICEINTABLE>
       <DEVICEOUTTABLE v-if="config.code === 'DEVICE_OUT_TABLE'" :config="config" @change="change"></DEVICEOUTTABLE>
-      <TABLE v-if="config.code === 'TABLE'" :config="config" @change="change"></TABLE>
+      <TABLE v-if="config.code === 'TABLE'" :config="config" @change="change"></TABLE> -->
+      <SIGN :config="config" @change="change"></SIGN>
     </view>
   </u-form-item>
 </template>
@@ -34,10 +35,11 @@
   import DEVICEINTABLE from './DEVICE_IN_TABLE.vue'
   import DEVICEOUTTABLE from './DEVICE_OUT_TABLE.vue'
   import TABLE from './TABLE.vue'
+  import SIGN from './SIGN.vue'
   export default {
     name: 'MyFormItem',
     props: ['config', 'change1', 'change2'],
-    components: { TEXT, SELECT, MULTIPLESELECT, DATE, RADIO, CHECKBOX, UPLOADIMAGE, RADIOWITHINPUT, DEVICEINTABLE, DEVICEOUTTABLE, TABLE },
+    components: { TEXT, SELECT, MULTIPLESELECT, DATE, RADIO, CHECKBOX, UPLOADIMAGE, RADIOWITHINPUT, DEVICEINTABLE, DEVICEOUTTABLE, TABLE, SIGN },
     computed: {
       isBigTitle() {
         return this.bigTitles.includes(this.config.code)

+ 327 - 0
src/components/my-sign.vue

@@ -0,0 +1,327 @@
+<template>
+  <view class="signature-wrap">
+    <canvas
+      ref="canvas"
+      :canvas-id="cid"
+      :id="cid"
+      @touchstart="onTouchStart"
+      @touchmove="onTouchMove"
+      @touchend="onTouchEnd"
+      disable-scroll
+      :style="[
+        {
+          width: width && formatSize(width),
+          height: height && formatSize(height)
+        },
+        customStyle
+      ]"
+    ></canvas>
+    <slot />
+  </view>
+</template>
+
+<script>
+  /**
+   * sign canvas 手写签名
+   * @description 设置线条宽度、颜色,撤回,清空
+   * @tutorial
+   * @property {String} cid canvas id 不设置则默认为 v-sign-时间戳
+   * @property {String, Number} width canvas 宽度
+   * @property {String, Number} height canvas 高度
+   * @property {bgColor} bgColor 画布背景颜色
+   * @property {Object} customStyle canvas 自定义样式
+   * @property {String} lineWidth 画笔大小,权重小于 v-sign-pen 组件设置的画笔大小
+   * @property {Number} lineColor 画笔颜色,权重小于 v-sign-pen 组件设置的画笔大小
+   * @event {Function} init 当创建完 canvas 实例后触发,向外提供 canvas实例,撤回,清空方法
+   * @example <v-sign @init="signInit"></v-sign>
+   */
+  import { formatSize } from '@/utils/utils'
+
+  export default {
+    name: 'my-sign',
+    props: {
+      // canvas id
+      cid: {
+        type: String,
+        default: `v-sign-${Date.now()}`
+        // required: true
+      },
+      // canvas 宽度
+      width: {
+        type: [String, Number]
+      },
+      // canvas 高度
+      height: {
+        type: [String, Number]
+      },
+      // 画笔大小,权重小于 v-sign-pen 组件设置的画笔大小 penLineWidth
+      lineWidth: {
+        type: Number,
+        default: 4
+      },
+      // 线颜色,权重小于 v-sign-color 组件设置的画笔颜色 penLineColor
+      lineColor: {
+        type: String,
+        default: '#333'
+      },
+      // 画布背景颜色
+      bgColor: {
+        type: String,
+        default: '#fff'
+      },
+      // canvas自定义样式
+      customStyle: {
+        type: Object,
+        default: () => ({})
+      }
+    },
+    provide() {
+      return {
+        getSignInterface: this.provideSignInterface
+      }
+    },
+    data() {
+      return {
+        formatSize,
+        lineData: [],
+        winWidth: 0,
+        winHeight: 0,
+        penLineWidth: null, // v-sign-pen 组件设置的画笔大小
+        penLineColor: null // v-sign-color 组件设置的颜色
+      }
+    },
+    created() {
+      // 获取窗口宽高
+      const { windowWidth, windowHeight } = uni.getSystemInfoSync()
+      this.winWidth = windowWidth
+      this.winHeight = windowHeight
+    },
+    mounted() {
+      this.canvasCtx = uni.createCanvasContext(this.cid, this)
+      // h5 需延迟绘制,否则绘制失败
+      // #ifdef H5
+      setTimeout(() => {
+        // #endif
+        this.setBackgroundColor(this.bgColor)
+        // #ifdef H5
+      }, 10)
+      // #endif
+      // 初始化完成,触发 init 事件
+      this.$emit('init', this.provideSignInterface())
+    },
+    methods: {
+      onTouchStart(e) {
+        const pos = e.touches[0]
+        this.lineData.push({
+          style: {
+            color: this.penLineColor || this.lineColor,
+            width: this.penLineWidth || this.lineWidth
+          },
+          // 屏幕坐标
+          coordinates: [
+            {
+              type: e.type,
+              x: pos.x,
+              y: pos.y
+            }
+          ]
+        })
+        this.drawLine()
+      },
+      onTouchMove(e) {
+        const pos = e.touches[0]
+        this.lineData[this.lineData.length - 1].coordinates.push({
+          type: e.type,
+          x: pos.x,
+          y: pos.y
+        })
+        this.drawLine()
+      },
+      onTouchEnd(e) {
+        this.$emit('end', this.lineData)
+      },
+      // 清空画布
+      clear() {
+        this.lineData = []
+        this.canvasCtx.clearRect(0, 0, this.winWidth, this.winHeight)
+        this.canvasCtx.draw()
+        this.setBackgroundColor(this.bgColor)
+        this.$emit('clear')
+      },
+      // 撤销
+      revoke() {
+        this.setBackgroundColor(this.bgColor)
+        this.lineData.pop()
+        this.lineData.forEach((item, index) => {
+          this.canvasCtx.beginPath()
+          this.canvasCtx.setLineCap('round')
+          this.canvasCtx.setStrokeStyle(item.style.color)
+          this.canvasCtx.setLineWidth(item.style.width)
+          if (item.coordinates.length < 2) {
+            const pos = item.coordinates[0]
+            this.canvasCtx.moveTo(pos.x, pos.y)
+            this.canvasCtx.lineTo(pos.x + 1, pos.y)
+          } else {
+            item.coordinates.forEach((pos) => {
+              if (pos.type == 'touchstart') {
+                this.canvasCtx.moveTo(pos.x, pos.y)
+              } else {
+                this.canvasCtx.lineTo(pos.x, pos.y)
+              }
+            })
+          }
+          this.canvasCtx.stroke()
+        })
+        this.canvasCtx.draw(true)
+        this.$emit('revoke', this.lineData)
+      },
+      // 绘制线条
+      drawLine() {
+        const lineDataLen = this.lineData.length
+        if (!lineDataLen) return
+        const currentLineData = this.lineData[lineDataLen - 1]
+        const coordinates = currentLineData.coordinates
+        const coordinatesLen = coordinates.length
+        if (!coordinatesLen) return
+        let startPos
+        let endPos
+        if (coordinatesLen < 2) {
+          // only start, no move event
+          startPos = coordinates[coordinatesLen - 1]
+          endPos = {
+            x: startPos.x + 1,
+            y: startPos.y
+          }
+        } else {
+          startPos = coordinates[coordinatesLen - 2]
+          endPos = coordinates[coordinatesLen - 1]
+        }
+
+        const style = currentLineData.style
+        this.canvasCtx.beginPath()
+        this.canvasCtx.setLineCap('round')
+        this.canvasCtx.setStrokeStyle(style.color)
+        this.canvasCtx.setLineWidth(style.width)
+        this.canvasCtx.moveTo(startPos.x, startPos.y)
+        this.canvasCtx.lineTo(endPos.x, endPos.y)
+        // const P1 = this.caculateBezier(startPos, endPos, centerPos)
+        // console.log(P1.x, P1.y)
+        // this.canvasCtx.moveTo(startPos.x, startPos.y)
+        // this.canvasCtx.quadraticCurveTo(P1.x, P1.y, endPos.x, endPos.y)
+        this.canvasCtx.stroke()
+        this.canvasCtx.draw(true)
+      },
+      // 保存png图片,文件名配置 filename 仅支持 h5
+      async saveImage(filename = '签名') {
+        const tempFilePath = await this.canvasToTempFilePath()
+        return new Promise((resolve, reject) => {
+          // #ifdef H5
+          try {
+            const a = document.createElement('a')
+            a.href = tempFilePath
+            a.download = filename
+            document.body.appendChild(a)
+            a.click()
+            a.remove()
+            resolve({
+              errMsg: 'saveImageH5:ok'
+            })
+          } catch (e) {
+            console.error(e)
+            reject(e)
+          }
+          // #endif
+          // #ifndef H5
+          uni.saveImageToPhotosAlbum({
+            filePath: tempFilePath,
+            success(resObj) {
+              resolve(resObj)
+            },
+            fail(err) {
+              reject(err)
+            }
+          })
+          // #endif
+        })
+      },
+      // canvas 保存为临时图片路径,h5返回 base64
+      canvasToTempFilePath(conf = {}) {
+        return new Promise((resolve, reject) => {
+          uni.canvasToTempFilePath(
+            {
+              canvasId: this.cid,
+              ...conf,
+              success: (res) => {
+                resolve(res.tempFilePath)
+              },
+              fail: (err) => {
+                console.log('fail', err)
+                reject(err)
+              }
+            },
+            this
+          )
+        })
+      },
+      setBackgroundColor(color = '#fff') {
+        this.canvasCtx.beginPath()
+        this.canvasCtx.setFillStyle(color)
+        this.canvasCtx.fillRect(0, 0, this.winWidth, this.winHeight)
+        this.canvasCtx.fill()
+        this.canvasCtx.draw(true)
+      },
+      setLineWidth(numberVal) {
+        this.penLineWidth = numberVal
+      },
+      setLineColor(strValue) {
+        this.penLineColor = strValue
+      },
+      // 向外暴露内部方法
+      provideSignInterface() {
+        return {
+          cid: this.cid,
+          ctx: this.canvasCtx,
+          clear: this.clear,
+          revoke: this.revoke,
+          saveImage: this.saveImage,
+          canvasToTempFilePath: this.canvasToTempFilePath,
+          setLineWidth: this.setLineWidth,
+          setLineColor: this.setLineColor,
+          setBackgroundColor: this.setBackgroundColor,
+          getLineData: () => this.lineData,
+          canvas2base64: this.canvas2base64
+        }
+      },
+      /**
+       * 计算二次贝塞尔曲线 控制点 P1
+       * 起点 P0(x0,y0)、控制点P1(x1, y1)、P2(x2, y2)、曲线上任意点B(x, y)
+       * 二次贝塞尔公式:B(t) = (1-t)²P0 + 2t(1-t)P1 + t²P2
+       * 代入坐标得:
+       * x = (1-t)²*x0 + 2t(1-t)*x1 + t²*x2
+       * y = (1-t)²*y0 + 2t(1-t)*y1 + t²*y2
+       */
+      caculateBezier(P0, P2, B, t = 0.5) {
+        const { x: x0, y: y0 } = P0
+        const { x: x2, y: y2 } = P2
+        const { x, y } = B
+        let x1 = (x - (1 - t) * (1 - t) * x0 - t * t * x2) / (2 * t * (1 - t))
+        let y1 = (y - (1 - t) * (1 - t) * y0 - t * t * y2) / (2 * t * (1 - t))
+        return {
+          x: x1,
+          y: y1
+        }
+      },
+      canvas2base64() {
+        // 保存签名图片
+        const dataURL = this.$refs.canvas.toDataURL('image/png')
+        console.log(dataURL)
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .signature-wrap {
+    position: relative;
+  }
+</style>

+ 8 - 0
src/pages.json

@@ -35,6 +35,14 @@
         "enablePullDownRefresh": false
       }
     },
+    {
+      "path": "pages/sign/sign",
+      "style": {
+        "navigationBarTitleText": "手写签名",
+        "enablePullDownRefresh": false,
+        "pageOrientation":"landscape"
+      }
+    },
     {
       "path": "pages/sop/project-change-report/plan-change",
       "style": {

+ 89 - 0
src/pages/sign/sign.vue

@@ -0,0 +1,89 @@
+<template>
+  <view class="sign">
+    <view class="head">请在下方区域内横向书写</view>
+    <MySign ref="mySign" width="100vw" height="calc(100vh - 110rpx)"></MySign>
+    <view class="foot flex justify-end items-center">
+      <u-button size="mini" @click="reset">重写</u-button>
+      <u-button type="primary" size="mini" class="m-l-20rpx" @click="confirm">确定</u-button>
+    </view>
+  </view>
+</template>
+
+<script>
+  import MySign from '@/components/my-sign.vue'
+  import { getWxFileMD5, DEVICE_ID, getAuthorization } from '@/utils/crypto'
+  export default {
+    name: 'Sign',
+    components: { MySign },
+    props: ['config'],
+    methods: {
+      reset() {
+        this.$refs.mySign.clear()
+      },
+      confirm() {
+        this.$refs.mySign.canvasToTempFilePath().then((res) => {
+          console.log('rr', res)
+          getWxFileMD5(res).then((md5) => {
+            let user = uni.getStorageSync('user')
+            let timestamp = Date.now()
+            const authorization = getAuthorization(
+              {
+                method: 'post',
+                uri: '/api/admin/common/file/upload',
+                timestamp,
+                sessionId: user.sessionId,
+                token: user.accessToken
+              },
+              'token'
+            )
+            console.log('开始上传:', authorization)
+            uni.uploadFile({
+              url: process.env.VUE_APP_BASE_API + '/api/admin/common/file/upload',
+              filePath: res,
+              name: 'file',
+              formData: { type: 'FILE' },
+              header: {
+                'Content-Type': 'multipart/form-data',
+                md5,
+                deviceId: DEVICE_ID,
+                platform: 'WEB',
+                Authorization: authorization,
+                time: timestamp
+              },
+              success: (res) => {
+                console.log('uploadFileRes:', res.data, typeof res.data)
+                const response = JSON.parse(res.data)
+                // this.$emit('change', { prop: this.config.formName, value: res.data.previewUrl })
+                uni.$emit('getSignImgOssUrl', { id: this.$Route.query.id, imgUrl: response.data.previewUrl })
+                if (this.$Route.query.id) {
+                  this.$Router.back()
+                }
+              },
+              fail: () => {
+                this.$u.toast('签名图片上传失败')
+              }
+            })
+          })
+        })
+      }
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .sign {
+    width: 100vw;
+    height: 100vh;
+    background-color: #fff;
+    .head {
+      height: 40rpx;
+      color: #595959;
+      font-size: 24rpx;
+      padding: 0 24rpx;
+    }
+    .foot {
+      height: 70rpx;
+      padding: 0 24rpx;
+    }
+  }
+</style>

+ 2 - 24
src/pages/sop-step/sop-step.vue

@@ -134,7 +134,7 @@
             },
             ARRAY: {
               validator: (rule, value, callback) => {
-                return Array.isArray(value) && value.length
+                return Array.isArray(value) && !!value.length
               },
               message: item.code === 'FILE' ? '请上传图片' : '请至少填写一项'
             }
@@ -163,28 +163,6 @@
     },
     methods: {
       submit(approve = 'START') {
-        // const formProperties = cloneDeep(this.curFormConfig)
-        // const formPropertiesValueJsonWithValueKey = formProperties.map((item) => {
-        //   item.value = JSON.stringify({ value: this.formData[item.formName] })
-        //   if (Array.isArray(item.options)) {
-        //     item.options = JSON.stringify(item.options)
-        //   }
-        //   return item
-        // })
-        // console.log('formPropertiesValueJsonWithValueKey:', formPropertiesValueJsonWithValueKey)
-        // sopApproveApi({
-        //   taskId: this.taskId,
-        //   formProperties: formPropertiesValueJsonWithValueKey,
-        //   setup: this.setup,
-        //   approve
-        // }).then(() => {
-        //   this.$refs.uToast.show({
-        //     title: '填报成功',
-        //     type: 'success'
-        //   })
-        //   this.$Router.back()
-        // })
-        // console.log('rrr', this.rules)
         this.$refs.uForm.validate((valid) => {
           if (valid) {
             console.log('验证通过')
@@ -224,7 +202,7 @@
       },
       init() {
         getSopFlowView({ flowId: this.flowId }).then((res) => {
-          // res = { crmInfo: res.crmInfo, ...testData }
+          res = { crmInfo: res.crmInfo, ...testData }
           this.crmInfo = res.crmInfo || {}
           this.setup = res.currFlowTaskResult?.setup
           res.flowTaskHistoryList = res.flowTaskHistoryList || []

+ 3 - 64
src/pages/sop-step/test.js

@@ -28,70 +28,9 @@ export default {
     formKey: 'XXX',
     formProperty: [
       {
-        code: 'TABLE',
-        title: '这是一个TABLE组件',
-        tablePropList: [
-          {
-            id: 'XXX1',
-            widgetId: 'XXX',
-            tdIndex: 1,
-            tdId: 'XXX',
-            tdName: 'XXX1',
-            title: '学院/分(子)机构',
-            tdOrder: true,
-            tdSearch: true,
-            editWidgetId: '19',
-            tdFormWidget: {
-              code: 'TEXT',
-              name: '文本'
-            }
-          },
-          {
-            id: 'XXX2',
-            widgetId: 'XXX',
-            tdIndex: 2,
-            tdId: 'XXX2',
-            tdName: 'XXX2',
-            title: '姓名',
-            tdOrder: true,
-            tdSearch: true,
-            editWidgetId: 'XXX',
-            tdFormWidget: {
-              code: 'TEXT',
-              name: '文本'
-            }
-          },
-          {
-            id: 'XXX3',
-            widgetId: 'XXX',
-            tdIndex: 3,
-            tdId: 'XXX',
-            tdName: 'XXX3',
-            title: '职务',
-            tdOrder: true,
-            tdSearch: true,
-            editWidgetId: 'XXX',
-            tdFormWidget: {
-              code: 'TEXT',
-              name: '文本'
-            }
-          },
-          {
-            id: 'XXX4',
-            widgetId: 'XXX',
-            tdIndex: 4,
-            tdId: 'XXX',
-            tdName: 'XXX4',
-            title: '手机',
-            tdOrder: true,
-            tdSearch: true,
-            editWidgetId: 'XXX',
-            tdFormWidget: {
-              code: 'TEXT',
-              name: '文本'
-            }
-          }
-        ]
+        id: '12345',
+        code: 'SIGN',
+        title: '手写签名'
       }
       // {
       //   code: 'RADIO',

+ 5 - 1
src/pages/sop/office-sop-list.vue → src/pages/sop/sop-list.vue

@@ -45,11 +45,12 @@
   import { dateFormat } from '@/utils/utils'
   export default {
     name: 'OfficeSopList',
+    props: ['type'],
     data() {
       return {
         dateFormat,
         dictToOptionList,
-        params: { serviceId: '', type: 'OFFICE_SOP_FLOW' },
+        params: { serviceId: '', type: '' },
         pageNumber: 1,
         pageSize: 10,
         loadingFlag: 0,
@@ -57,6 +58,9 @@
         triggered: false
       }
     },
+    created() {
+      this.params.type = this.type
+    },
     mounted() {
       this.search()
     },

+ 4 - 3
src/pages/sop/sop.vue

@@ -1,7 +1,8 @@
 <template>
   <view class="sop">
     <u-tabs :list="tabList" :is-scroll="true" :current="current" @change="tabChange"></u-tabs>
-    <officeSopList v-if="current == 0"></officeSopList>
+    <SopList v-if="current == 0" type="OFFICE_SOP_FLOW"></SopList>
+    <SopList v-if="current == 1" type="CLOUD_MARK_SOP_FLOW"></SopList>
     <ProjectChangeReport v-if="current == 2"></ProjectChangeReport>
     <DelayWarning v-if="current == 3"></DelayWarning>
     <ViolationRegistration v-if="current == 4"></ViolationRegistration>
@@ -9,14 +10,14 @@
 </template>
 
 <script>
-  import OfficeSopList from './office-sop-list.vue'
+  import SopList from './sop-list.vue'
   import ProjectChangeReport from './project-change-report/index.vue'
   import DelayWarning from './delay-warning/index.vue'
   import ViolationRegistration from './violation-registration/index.vue'
 
   export default {
     name: 'Sop',
-    components: { OfficeSopList, ProjectChangeReport, DelayWarning, ViolationRegistration },
+    components: { SopList, ProjectChangeReport, DelayWarning, ViolationRegistration },
     data() {
       return {
         tabList: [{ name: '教务处SOP' }, { name: '研究生SOP' }, { name: '项目计划变更' }, { name: '延期预警' }, { name: '违规登记' }],

+ 15 - 0
src/utils/crypto.js

@@ -73,3 +73,18 @@ export const getFileMD5 = (dataFile) => {
     }
   })
 }
+
+export const getWxFileMD5 = (path) => {
+  return new Promise((resolve, reject) => {
+    wx.getFileSystemManager().readFile({
+      filePath: path,
+      encoding: 'binary',
+      success: (res) => {
+        let spark = new SparkMD5()
+        spark.appendBinary(res.data)
+        var md5 = spark.end()
+        resolve(md5)
+      }
+    })
+  })
+}

+ 16 - 0
src/utils/utils.js

@@ -660,3 +660,19 @@ export const timeCompare = (startStr, time, endStr) => {
   let end = new Date(endStr) //结束时间
   return start.getTime() > time && time < end.getTime()
 }
+
+/**
+ * 判断是否未数值
+ * @param {Object} val
+ */
+export function isNumber(val) {
+  return !isNaN(Number(val))
+}
+
+/**
+ * 处理大小单位
+ * @param {Object} val
+ */
+export function formatSize(val, unit = 'rpx') {
+  return isNumber(val) ? `${val}${unit}` : val
+}