mo 2 éve
szülő
commit
ce05ddf9b7

+ 430 - 426
src/components/o-cropper/index.tsx

@@ -1,426 +1,430 @@
-import { defineComponent } from 'vue'
-import 'vue-cropper/dist/index.css'
-import { VueCropper } from 'vue-cropper'
-import styles from './index.module.less'
-
-export default defineComponent({
-  name: 'o-cropper',
-  components: { VueCropper },
-  props: {
-    hideInput: {
-      type: Boolean,
-      default: false
-    },
-    option: {
-      type: Object
-    },
-    onCancelTailor: {
-      type: Function,
-      default: () => {}
-    }, // 取消
-    getBase64Data: {
-      type: Function,
-      default: () => {}
-    },
-    getBlob: {
-      type: Function,
-      default: () => {}
-    },
-    getFile: {
-      type: Function,
-      default: () => {}
-    },
-    imgOriginF: {
-      type: Function,
-      default: () => {}
-    }
-  },
-  data() {
-    return {
-      img: '',
-      config: {
-        ceilbutton: false, //顶部按钮,默认底部
-        outputSize: 1, //裁剪生成图片的质量
-        outputType: 'png', //裁剪生成图片的格式,默认png
-        info: false, //裁剪框的大小信息
-        canScale: true, //图片是否允许滚轮缩放
-        autoCrop: false, //是否默认生成截图框
-        autoCropWidth: 0, //默认生成截图框宽度
-        autoCropHeight: 0, //默认生成截图框高度
-        fixed: true, //是否开启截图框宽高固定比例
-        fixedNumber: [1, 1], //截图框的宽高比例
-        full: false, //是否输出原图比例的截图
-        fixedBox: true, //固定截图框大小 不允许改变
-        canMove: true, //上传图片是否可以移动
-        canMoveBox: false, //截图框能否拖动
-        original: false, //上传图片按照原始比例渲染
-        centerBox: true, //截图框是否被限制在图片里面
-        high: true, //是否按照设备的dpr 输出等比例图片
-        infoTrue: false, //true 为展示真实输出图片宽高 false 展示看到的截图框宽高
-        maxImgSize: 2000, //限制图片最大宽度和高度
-        enlarge: 1, //图片根据截图框输出比例倍数
-        mode: '100%', //图片默认渲染方式
-        cancelButtonText: '取消', //取消按钮文本
-        confirmButtonText: '确定', //确定按钮文本
-        cancelButtonBackgroundColor: '#606266', //取消按钮背景色
-        confirmButtonBackgroundColor: '#ed594c', //确定按钮背景色
-        cancelButtonTextColor: '#ffffff', //取消按钮字体色
-        confirmButtonTextColor: '#ffffff' //确定按钮字体色
-      }
-    }
-  },
-  mounted() {
-    this.config = Object.assign(this.config, this.option)
-  },
-  methods: {
-    //添加网格线
-    addSlide() {
-      if (document.getElementById('vertical') == null) {
-        const box = document.getElementsByClassName('cropper-crop-box')[0]
-        //左网格线
-        const verticalLeft = document.createElement('div')
-        verticalLeft.id = 'vertical'
-        verticalLeft.style.width = '1px'
-        verticalLeft.style.height = '100%'
-        verticalLeft.style.top = '0px'
-        verticalLeft.style.left = '33%'
-        verticalLeft.style.position = 'absolute'
-        verticalLeft.style.backgroundColor = '#fff'
-        verticalLeft.style.zIndex = '522'
-        verticalLeft.style.opacity = '0.5'
-        //右网格线
-        const verticalRight = document.createElement('div')
-        verticalRight.style.width = '1px'
-        verticalRight.style.height = '100%'
-        verticalRight.style.top = '0px'
-        verticalRight.style.right = '33%'
-        verticalRight.style.position = 'absolute'
-        verticalRight.style.backgroundColor = '#fff'
-        verticalRight.style.zIndex = '522'
-        verticalRight.style.opacity = '0.5'
-        //上网格线
-        const verticalTop = document.createElement('div')
-        verticalTop.style.width = '100%'
-        verticalTop.style.height = '1px'
-        verticalTop.style.top = '33%'
-        verticalTop.style.left = '0px'
-        verticalTop.style.position = 'absolute'
-        verticalTop.style.backgroundColor = '#fff'
-        verticalTop.style.zIndex = '522'
-        verticalTop.style.opacity = '0.5'
-        //下网格线
-        const verticalBottom = document.createElement('div')
-        verticalBottom.style.width = '100%'
-        verticalBottom.style.height = '1px'
-        verticalBottom.style.bottom = '33%'
-        verticalBottom.style.left = '0px'
-        verticalBottom.style.position = 'absolute'
-        verticalBottom.style.backgroundColor = '#fff'
-        verticalBottom.style.zIndex = '522'
-        verticalBottom.style.opacity = '0.5'
-        //左上边线
-        const LeftTopSide = document.createElement('div')
-        LeftTopSide.style.width = '30px'
-        LeftTopSide.style.height = '4px'
-        LeftTopSide.style.top = '-4px'
-        LeftTopSide.style.left = '-4px'
-        LeftTopSide.style.position = 'absolute'
-        LeftTopSide.style.backgroundColor = '#fff'
-        LeftTopSide.style.zIndex = '522'
-        LeftTopSide.style.opacity = '1'
-        //上左边线
-        const TopListSide = document.createElement('div')
-        TopListSide.style.width = '4px'
-        TopListSide.style.height = '30px'
-        TopListSide.style.top = '-4px'
-        TopListSide.style.left = '-4px'
-        TopListSide.style.position = 'absolute'
-        TopListSide.style.backgroundColor = '#fff'
-        TopListSide.style.zIndex = '522'
-        TopListSide.style.opacity = '1'
-        //右上边线
-        const RightTopSide = document.createElement('div')
-        RightTopSide.style.width = '30px'
-        RightTopSide.style.height = '4px'
-        RightTopSide.style.top = '-4px'
-        RightTopSide.style.right = '-4px'
-        RightTopSide.style.position = 'absolute'
-        RightTopSide.style.backgroundColor = '#fff'
-        RightTopSide.style.zIndex = '522'
-        RightTopSide.style.opacity = '1'
-        //上右边线
-        const TopRightSide = document.createElement('div')
-        TopRightSide.style.width = '4px'
-        TopRightSide.style.height = '30px'
-        TopRightSide.style.top = '-4px'
-        TopRightSide.style.right = '-4px'
-        TopRightSide.style.position = 'absolute'
-        TopRightSide.style.backgroundColor = '#fff'
-        TopRightSide.style.zIndex = '522'
-        TopRightSide.style.opacity = '1'
-        //左下边线
-        const LeftBottomSide = document.createElement('div')
-        LeftBottomSide.style.width = '30px'
-        LeftBottomSide.style.height = '4px'
-        LeftBottomSide.style.bottom = '-4px'
-        LeftBottomSide.style.left = '-4px'
-        LeftBottomSide.style.position = 'absolute'
-        LeftBottomSide.style.backgroundColor = '#fff'
-        LeftBottomSide.style.zIndex = '522'
-        LeftBottomSide.style.opacity = '1'
-        //下左边线
-        const BottomListSide = document.createElement('div')
-        BottomListSide.style.width = '4px'
-        BottomListSide.style.height = '30px'
-        BottomListSide.style.bottom = '-4px'
-        BottomListSide.style.left = '-4px'
-        BottomListSide.style.position = 'absolute'
-        BottomListSide.style.backgroundColor = '#fff'
-        BottomListSide.style.zIndex = '522'
-        BottomListSide.style.opacity = '1'
-        //右下边线
-        const RightBottomSide = document.createElement('div')
-        RightBottomSide.style.width = '30px'
-        RightBottomSide.style.height = '4px'
-        RightBottomSide.style.bottom = '-4px'
-        RightBottomSide.style.right = '-4px'
-        RightBottomSide.style.position = 'absolute'
-        RightBottomSide.style.backgroundColor = '#fff'
-        RightBottomSide.style.zIndex = '522'
-        RightBottomSide.style.opacity = '1'
-        //下右边线
-        const BottomRightSide = document.createElement('div')
-        BottomRightSide.style.width = '4px'
-        BottomRightSide.style.height = '30px'
-        BottomRightSide.style.bottom = '-4px'
-        BottomRightSide.style.right = '-4px'
-        BottomRightSide.style.position = 'absolute'
-        BottomRightSide.style.backgroundColor = '#fff'
-        BottomRightSide.style.zIndex = '522'
-        BottomRightSide.style.opacity = '1'
-        //一起生成
-        box.appendChild(verticalLeft)
-        box.appendChild(verticalRight)
-        box.appendChild(verticalTop)
-        box.appendChild(verticalBottom)
-        box.appendChild(LeftTopSide)
-        box.appendChild(TopListSide)
-        box.appendChild(RightTopSide)
-        box.appendChild(TopRightSide)
-        box.appendChild(LeftBottomSide)
-        box.appendChild(BottomListSide)
-        box.appendChild(RightBottomSide)
-        box.appendChild(BottomRightSide)
-      }
-    },
-    //异步onload图片
-    onLoadImg(photoUrl: any) {
-      return new Promise(function (resolve, reject) {
-        const reader = new FileReader()
-        reader.readAsDataURL(photoUrl)
-        reader.onload = (e: any) => {
-          resolve(e.target['result'])
-        }
-      })
-    },
-    /**
-     * 载入文件
-     * template:
-     *    <h5-cropper hide-input ref="cropper">
-     *
-     * javascript:
-     *    this.$refs.cropper.loadFile()
-     *
-     * @param file
-     */
-    loadFile(file: any) {
-      if (file instanceof File) {
-        this.onLoadImg(file).then((base64: any) => {
-          this.img = base64
-          setTimeout(() => {
-            this.config.autoCrop = true
-            this.addSlide()
-          }, 10)
-        })
-      } else {
-        throw new Error('Arguments file is not File')
-      }
-    },
-    /**
-     *
-     * @param base64
-     */
-    loadBase64(base64: string) {
-      if (typeof base64 !== 'string') {
-        throw new Error('Arguments base64 is not string')
-      }
-      const base = base64.split(',')
-      if (!/^data:image\/(.*?);base64$/.test(base[0])) {
-        throw new Error('Arguments base64 MIME is not image/*')
-      }
-      // Base64 Regex @see https://learnku.com/articles/42295
-      if (!/^[\/]?([\da-zA-Z]+[\/+]+)*[\da-zA-Z]+([+=]{1,2}|[\/])?$/.test(base[1])) {
-        throw new Error('Not standard base64')
-      }
-      this.img = base64
-      setTimeout(() => {
-        this.config.autoCrop = true
-        this.addSlide()
-      }, 10)
-    },
-    rotating(e: any) {
-      ;(this as any).$refs.cropper.rotateRight()
-      // document.getElementsByClassName("cropper-modal")[0].style = "background-color: rgba(0,0,0,0.5);transition: 0.88s";
-    },
-    canceltailor() {
-      this.img = ''
-      this.onCancelTailor()
-    },
-    tailoring() {
-      // 获取截图的base64数据
-      ;(this as any).$refs.cropper.getCropData((data: any) => {
-        this.getBase64Data(data)
-        this.getBlob(data)
-        this.img = ''
-        this.config.autoCrop = false
-      })
-      // 获取截图的blob数据
-      ;(this as any).$refs.cropper.getCropBlob((data: BlobPart) => {
-        this.getBase64Data(data)
-        this.getBlob(data)
-        // Blob 转 File
-        const suffix = {
-          jpeg: 'jpg',
-          png: 'png',
-          webp: 'webp'
-        }[this.config.outputType]
-        const time = new Date().getTime()
-        const file = new File([data], `${time}.${suffix}`, {
-          type: `image/${this.config.outputType}`
-        })
-        this.getFile(file)
-        this.img = ''
-        this.config.autoCrop = false
-      })
-    },
-    async upPhoto(e: any) {
-      let photoUrl = e.target.files[0]
-      ;(this as any).$refs.headInput.value = null
-      if (photoUrl != undefined) {
-        this.imgOriginF(photoUrl)
-        this.img = (await this.onLoadImg(photoUrl)) as string
-        this.config.autoCrop = true
-        setTimeout(() => {
-          this.addSlide()
-        }, 20)
-      }
-    },
-    onCropMoving(e: any) {
-      // console.log('onCropMoving')
-    },
-    onImgMoving(e: any) {
-      // console.log('onCropMoving')
-    }
-  },
-  render() {
-    return (
-      <div class={[styles.upbtn, styles.uploadWarper]}>
-        {this.hideInput}
-        {!this.hideInput ? (
-          <input
-            style="opacity: 0;"
-            class={styles.upbtn}
-            type="file"
-            accept="image/*"
-            onChange={this.upPhoto}
-            ref="headInput"
-          />
-        ) : null}
-        {this.img != '' ? (
-          <div class={styles.bg}>
-            {this.config.ceilbutton ? (
-              <div class={styles.btndiv}>
-                <div
-                  class={styles.btn}
-                  onClick={this.canceltailor}
-                  style={{
-                    backgroundColor: this.config.cancelButtonBackgroundColor,
-                    color: this.config.cancelButtonTextColor
-                  }}
-                >
-                  {this.config.cancelButtonText}
-                </div>
-                <div class={styles.img} onClick={this.rotating}></div>
-                <div
-                  class={styles.btn}
-                  onClick={this.tailoring}
-                  style={{
-                    backgroundColor: this.config.confirmButtonBackgroundColor,
-                    color: this.config.confirmButtonTextColor
-                  }}
-                >
-                  {this.config.confirmButtonText}
-                </div>
-              </div>
-            ) : null}
-
-            <div class={styles.wrapper}>
-              <VueCropper
-                ref="cropper"
-                img={this.img}
-                outputSize={this.config.outputSize}
-                outputType={this.config.outputType}
-                info={this.config.info}
-                canScale={this.config.canScale}
-                autoCrop={this.config.autoCrop}
-                autoCropWidth={this.config.autoCropWidth}
-                autoCropHeight={this.config.autoCropHeight}
-                fixedBox={this.config.fixedBox}
-                fixed={this.config.fixed}
-                fixedNumber={this.config.fixedNumber}
-                full={this.config.full}
-                canMove={this.config.canMove}
-                canMoveBox={this.config.canMoveBox}
-                original={this.config.original}
-                centerBox={this.config.centerBox}
-                high={this.config.high}
-                infoTrue={this.config.infoTrue}
-                maxImgSize={this.config.maxImgSize}
-                enlarge={this.config.enlarge}
-                mode={this.config.mode}
-                onCropMoving={this.onCropMoving}
-                onImgMoving={this.onImgMoving}
-              ></VueCropper>
-            </div>
-
-            {!this.config.ceilbutton ? (
-              <div class={styles.btndiv}>
-                <div
-                  class={styles.btn}
-                  onClick={this.canceltailor}
-                  style={{
-                    backgroundColor: this.config.cancelButtonBackgroundColor,
-                    color: this.config.cancelButtonTextColor
-                  }}
-                >
-                  {this.config.cancelButtonText}
-                </div>
-                <div class={styles.img} onClick={this.rotating}></div>
-                <div
-                  class={styles.btn}
-                  onClick={this.tailoring}
-                  style={{
-                    backgroundColor: this.config.confirmButtonBackgroundColor,
-                    color: this.config.confirmButtonTextColor
-                  }}
-                >
-                  {this.config.confirmButtonText}
-                </div>
-              </div>
-            ) : null}
-          </div>
-        ) : null}
-      </div>
-    )
-  }
-})
+import { defineComponent } from 'vue'
+import 'vue-cropper/dist/index.css'
+import { VueCropper } from 'vue-cropper'
+import styles from './index.module.less'
+
+export default defineComponent({
+  name: 'o-cropper',
+  components: { VueCropper },
+  props: {
+    hideInput: {
+      type: Boolean,
+      default: false
+    },
+    option: {
+      type: Object
+    },
+    onCancelTailor: {
+      type: Function,
+      default: () => {}
+    }, // 取消
+    getBase64Data: {
+      type: Function,
+      default: () => {}
+    },
+    getBlob: {
+      type: Function,
+      default: () => {}
+    },
+    getFile: {
+      type: Function,
+      default: () => {}
+    },
+    imgOriginF: {
+      type: Function,
+      default: () => {}
+    }
+  },
+  emits: ['chioseFile'],
+  data() {
+    return {
+      img: '',
+      config: {
+        ceilbutton: false, //顶部按钮,默认底部
+        outputSize: 1, //裁剪生成图片的质量
+        outputType: 'png', //裁剪生成图片的格式,默认png
+        info: false, //裁剪框的大小信息
+        canScale: true, //图片是否允许滚轮缩放
+        autoCrop: false, //是否默认生成截图框
+        autoCropWidth: 0, //默认生成截图框宽度
+        autoCropHeight: 0, //默认生成截图框高度
+        fixed: true, //是否开启截图框宽高固定比例
+        fixedNumber: [1, 1], //截图框的宽高比例
+        full: false, //是否输出原图比例的截图
+        fixedBox: true, //固定截图框大小 不允许改变
+        canMove: true, //上传图片是否可以移动
+        canMoveBox: false, //截图框能否拖动
+        original: false, //上传图片按照原始比例渲染
+        centerBox: true, //截图框是否被限制在图片里面
+        high: true, //是否按照设备的dpr 输出等比例图片
+        infoTrue: false, //true 为展示真实输出图片宽高 false 展示看到的截图框宽高
+        maxImgSize: 2000, //限制图片最大宽度和高度
+        enlarge: 1, //图片根据截图框输出比例倍数
+        mode: '100%', //图片默认渲染方式
+        cancelButtonText: '取消', //取消按钮文本
+        confirmButtonText: '确定', //确定按钮文本
+        cancelButtonBackgroundColor: '#606266', //取消按钮背景色
+        confirmButtonBackgroundColor: '#ed594c', //确定按钮背景色
+        cancelButtonTextColor: '#ffffff', //取消按钮字体色
+        confirmButtonTextColor: '#ffffff' //确定按钮字体色
+      }
+    }
+  },
+  mounted() {
+    this.config = Object.assign(this.config, this.option)
+  },
+  methods: {
+    //添加网格线
+    addSlide() {
+      if (document.getElementById('vertical') == null) {
+        const box = document.getElementsByClassName('cropper-crop-box')[0]
+        //左网格线
+        const verticalLeft = document.createElement('div')
+        verticalLeft.id = 'vertical'
+        verticalLeft.style.width = '1px'
+        verticalLeft.style.height = '100%'
+        verticalLeft.style.top = '0px'
+        verticalLeft.style.left = '33%'
+        verticalLeft.style.position = 'absolute'
+        verticalLeft.style.backgroundColor = '#fff'
+        verticalLeft.style.zIndex = '522'
+        verticalLeft.style.opacity = '0.5'
+        //右网格线
+        const verticalRight = document.createElement('div')
+        verticalRight.style.width = '1px'
+        verticalRight.style.height = '100%'
+        verticalRight.style.top = '0px'
+        verticalRight.style.right = '33%'
+        verticalRight.style.position = 'absolute'
+        verticalRight.style.backgroundColor = '#fff'
+        verticalRight.style.zIndex = '522'
+        verticalRight.style.opacity = '0.5'
+        //上网格线
+        const verticalTop = document.createElement('div')
+        verticalTop.style.width = '100%'
+        verticalTop.style.height = '1px'
+        verticalTop.style.top = '33%'
+        verticalTop.style.left = '0px'
+        verticalTop.style.position = 'absolute'
+        verticalTop.style.backgroundColor = '#fff'
+        verticalTop.style.zIndex = '522'
+        verticalTop.style.opacity = '0.5'
+        //下网格线
+        const verticalBottom = document.createElement('div')
+        verticalBottom.style.width = '100%'
+        verticalBottom.style.height = '1px'
+        verticalBottom.style.bottom = '33%'
+        verticalBottom.style.left = '0px'
+        verticalBottom.style.position = 'absolute'
+        verticalBottom.style.backgroundColor = '#fff'
+        verticalBottom.style.zIndex = '522'
+        verticalBottom.style.opacity = '0.5'
+        //左上边线
+        const LeftTopSide = document.createElement('div')
+        LeftTopSide.style.width = '30px'
+        LeftTopSide.style.height = '4px'
+        LeftTopSide.style.top = '-4px'
+        LeftTopSide.style.left = '-4px'
+        LeftTopSide.style.position = 'absolute'
+        LeftTopSide.style.backgroundColor = '#fff'
+        LeftTopSide.style.zIndex = '522'
+        LeftTopSide.style.opacity = '1'
+        //上左边线
+        const TopListSide = document.createElement('div')
+        TopListSide.style.width = '4px'
+        TopListSide.style.height = '30px'
+        TopListSide.style.top = '-4px'
+        TopListSide.style.left = '-4px'
+        TopListSide.style.position = 'absolute'
+        TopListSide.style.backgroundColor = '#fff'
+        TopListSide.style.zIndex = '522'
+        TopListSide.style.opacity = '1'
+        //右上边线
+        const RightTopSide = document.createElement('div')
+        RightTopSide.style.width = '30px'
+        RightTopSide.style.height = '4px'
+        RightTopSide.style.top = '-4px'
+        RightTopSide.style.right = '-4px'
+        RightTopSide.style.position = 'absolute'
+        RightTopSide.style.backgroundColor = '#fff'
+        RightTopSide.style.zIndex = '522'
+        RightTopSide.style.opacity = '1'
+        //上右边线
+        const TopRightSide = document.createElement('div')
+        TopRightSide.style.width = '4px'
+        TopRightSide.style.height = '30px'
+        TopRightSide.style.top = '-4px'
+        TopRightSide.style.right = '-4px'
+        TopRightSide.style.position = 'absolute'
+        TopRightSide.style.backgroundColor = '#fff'
+        TopRightSide.style.zIndex = '522'
+        TopRightSide.style.opacity = '1'
+        //左下边线
+        const LeftBottomSide = document.createElement('div')
+        LeftBottomSide.style.width = '30px'
+        LeftBottomSide.style.height = '4px'
+        LeftBottomSide.style.bottom = '-4px'
+        LeftBottomSide.style.left = '-4px'
+        LeftBottomSide.style.position = 'absolute'
+        LeftBottomSide.style.backgroundColor = '#fff'
+        LeftBottomSide.style.zIndex = '522'
+        LeftBottomSide.style.opacity = '1'
+        //下左边线
+        const BottomListSide = document.createElement('div')
+        BottomListSide.style.width = '4px'
+        BottomListSide.style.height = '30px'
+        BottomListSide.style.bottom = '-4px'
+        BottomListSide.style.left = '-4px'
+        BottomListSide.style.position = 'absolute'
+        BottomListSide.style.backgroundColor = '#fff'
+        BottomListSide.style.zIndex = '522'
+        BottomListSide.style.opacity = '1'
+        //右下边线
+        const RightBottomSide = document.createElement('div')
+        RightBottomSide.style.width = '30px'
+        RightBottomSide.style.height = '4px'
+        RightBottomSide.style.bottom = '-4px'
+        RightBottomSide.style.right = '-4px'
+        RightBottomSide.style.position = 'absolute'
+        RightBottomSide.style.backgroundColor = '#fff'
+        RightBottomSide.style.zIndex = '522'
+        RightBottomSide.style.opacity = '1'
+        //下右边线
+        const BottomRightSide = document.createElement('div')
+        BottomRightSide.style.width = '4px'
+        BottomRightSide.style.height = '30px'
+        BottomRightSide.style.bottom = '-4px'
+        BottomRightSide.style.right = '-4px'
+        BottomRightSide.style.position = 'absolute'
+        BottomRightSide.style.backgroundColor = '#fff'
+        BottomRightSide.style.zIndex = '522'
+        BottomRightSide.style.opacity = '1'
+        //一起生成
+        box.appendChild(verticalLeft)
+        box.appendChild(verticalRight)
+        box.appendChild(verticalTop)
+        box.appendChild(verticalBottom)
+        box.appendChild(LeftTopSide)
+        box.appendChild(TopListSide)
+        box.appendChild(RightTopSide)
+        box.appendChild(TopRightSide)
+        box.appendChild(LeftBottomSide)
+        box.appendChild(BottomListSide)
+        box.appendChild(RightBottomSide)
+        box.appendChild(BottomRightSide)
+      }
+    },
+    //异步onload图片
+    onLoadImg(photoUrl: any) {
+      return new Promise(function (resolve, reject) {
+        const reader = new FileReader()
+        reader.readAsDataURL(photoUrl)
+        reader.onload = (e: any) => {
+          resolve(e.target['result'])
+        }
+      })
+    },
+    /**
+     * 载入文件
+     * template:
+     *    <h5-cropper hide-input ref="cropper">
+     *
+     * javascript:
+     *    this.$refs.cropper.loadFile()
+     *
+     * @param file
+     */
+    loadFile(file: any) {
+      if (file instanceof File) {
+        this.onLoadImg(file).then((base64: any) => {
+          this.img = base64
+          setTimeout(() => {
+            this.config.autoCrop = true
+            this.addSlide()
+          }, 10)
+        })
+      } else {
+        throw new Error('Arguments file is not File')
+      }
+    },
+    /**
+     *
+     * @param base64
+     */
+    loadBase64(base64: string) {
+      if (typeof base64 !== 'string') {
+        throw new Error('Arguments base64 is not string')
+      }
+      const base = base64.split(',')
+      if (!/^data:image\/(.*?);base64$/.test(base[0])) {
+        throw new Error('Arguments base64 MIME is not image/*')
+      }
+      // Base64 Regex @see https://learnku.com/articles/42295
+      if (!/^[\/]?([\da-zA-Z]+[\/+]+)*[\da-zA-Z]+([+=]{1,2}|[\/])?$/.test(base[1])) {
+        throw new Error('Not standard base64')
+      }
+      this.img = base64
+      setTimeout(() => {
+        this.config.autoCrop = true
+        this.addSlide()
+      }, 10)
+    },
+    rotating(e: any) {
+      ;(this as any).$refs.cropper.rotateRight()
+      // document.getElementsByClassName("cropper-modal")[0].style = "background-color: rgba(0,0,0,0.5);transition: 0.88s";
+    },
+    canceltailor() {
+      this.img = ''
+      this.onCancelTailor()
+    },
+    tailoring() {
+      // 获取截图的base64数据
+      ;(this as any).$refs.cropper.getCropData((data: any) => {
+        this.getBase64Data(data)
+        this.getBlob(data)
+        this.img = ''
+        this.config.autoCrop = false
+      })
+      // 获取截图的blob数据
+      ;(this as any).$refs.cropper.getCropBlob((data: BlobPart) => {
+        this.getBase64Data(data)
+        this.getBlob(data)
+        // Blob 转 File
+        const suffix = {
+          jpeg: 'jpg',
+          png: 'png',
+          webp: 'webp'
+        }[this.config.outputType]
+        const time = new Date().getTime()
+        const file = new File([data], `${time}.${suffix}`, {
+          type: `image/${this.config.outputType}`
+        })
+        this.getFile(file)
+        this.img = ''
+        this.config.autoCrop = false
+      })
+    },
+    async upPhoto(e: any) {
+      let photoUrl = e.target.files[0]
+      ;(this as any).$refs.headInput.value = null
+      if (photoUrl != undefined) {
+        this.imgOriginF(photoUrl)
+        this.img = (await this.onLoadImg(photoUrl)) as string
+        this.config.autoCrop = true
+        setTimeout(() => {
+          this.addSlide()
+        }, 20)
+      }
+    },
+    onCropMoving(e: any) {
+      // console.log('onCropMoving')
+    },
+    onImgMoving(e: any) {
+      // console.log('onCropMoving')
+    },
+    startChiose() {
+      ;(this as any).$refs.headInput.click()
+    }
+  },
+  render() {
+    return (
+      <div class={[styles.upbtn, styles.uploadWarper]}>
+        {this.hideInput}
+        {!this.hideInput ? (
+          <input
+            style="opacity: 0;"
+            class={styles.upbtn}
+            type="file"
+            accept="image/*"
+            onChange={this.upPhoto}
+            ref="headInput"
+          />
+        ) : null}
+        {this.img != '' ? (
+          <div class={styles.bg}>
+            {this.config.ceilbutton ? (
+              <div class={styles.btndiv}>
+                <div
+                  class={styles.btn}
+                  onClick={this.canceltailor}
+                  style={{
+                    backgroundColor: this.config.cancelButtonBackgroundColor,
+                    color: this.config.cancelButtonTextColor
+                  }}
+                >
+                  {this.config.cancelButtonText}
+                </div>
+                <div class={styles.img} onClick={this.rotating}></div>
+                <div
+                  class={styles.btn}
+                  onClick={this.tailoring}
+                  style={{
+                    backgroundColor: this.config.confirmButtonBackgroundColor,
+                    color: this.config.confirmButtonTextColor
+                  }}
+                >
+                  {this.config.confirmButtonText}
+                </div>
+              </div>
+            ) : null}
+
+            <div class={styles.wrapper}>
+              <VueCropper
+                ref="cropper"
+                img={this.img}
+                outputSize={this.config.outputSize}
+                outputType={this.config.outputType}
+                info={this.config.info}
+                canScale={this.config.canScale}
+                autoCrop={this.config.autoCrop}
+                autoCropWidth={this.config.autoCropWidth}
+                autoCropHeight={this.config.autoCropHeight}
+                fixedBox={this.config.fixedBox}
+                fixed={this.config.fixed}
+                fixedNumber={this.config.fixedNumber}
+                full={this.config.full}
+                canMove={this.config.canMove}
+                canMoveBox={this.config.canMoveBox}
+                original={this.config.original}
+                centerBox={this.config.centerBox}
+                high={this.config.high}
+                infoTrue={this.config.infoTrue}
+                maxImgSize={this.config.maxImgSize}
+                enlarge={this.config.enlarge}
+                mode={this.config.mode}
+                onCropMoving={this.onCropMoving}
+                onImgMoving={this.onImgMoving}
+              ></VueCropper>
+            </div>
+
+            {!this.config.ceilbutton ? (
+              <div class={styles.btndiv}>
+                <div
+                  class={styles.btn}
+                  onClick={this.canceltailor}
+                  style={{
+                    backgroundColor: this.config.cancelButtonBackgroundColor,
+                    color: this.config.cancelButtonTextColor
+                  }}
+                >
+                  {this.config.cancelButtonText}
+                </div>
+                <div class={styles.img} onClick={this.rotating}></div>
+                <div
+                  class={styles.btn}
+                  onClick={this.tailoring}
+                  style={{
+                    backgroundColor: this.config.confirmButtonBackgroundColor,
+                    color: this.config.confirmButtonTextColor
+                  }}
+                >
+                  {this.config.confirmButtonText}
+                </div>
+              </div>
+            ) : null}
+          </div>
+        ) : null}
+      </div>
+    )
+  }
+})

+ 33 - 33
src/components/o-empty/index.module.less

@@ -1,33 +1,33 @@
-.col-result {
-  padding: 30px 14px 14px;
-  text-align: center;
-  margin: 0 auto;
-  .tips {
-    font-size: 14px;
-    color: #333;
-    padding: 20px 0;
-  }
-  .btn {
-    width: 55%;
-    margin: 0 auto;
-  }
-  .SMALL {
-    :global {
-      .van-empty__image {
-        width: 161px;
-        height: 161px;
-      }
-    }
-  }
-  .CERT {
-    :global {
-      .van-empty__image {
-        width: 230px;
-        height: 230px;
-      }
-      .van-empty__description {
-        padding: 0 30px;
-      }
-    }
-  }
-}
+.col-result {
+  padding: 30px 14px 14px;
+  text-align: center;
+  margin: 0 auto;
+  .tips {
+    font-size: 14px;
+    color: #333;
+    padding: 20px 0;
+  }
+  .btn {
+    width: 55%;
+    margin: 0 auto;
+  }
+  .SMALL {
+    :global {
+      .van-empty__image {
+        width: 161px;
+        height: 161px;
+      }
+    }
+  }
+  .CERT {
+    :global {
+      .van-empty__image {
+        width: 230px;
+        height: 230px;
+      }
+      .van-empty__description {
+        padding: 0 30px;
+      }
+    }
+  }
+}

+ 242 - 229
src/components/o-upload/index.tsx

@@ -1,229 +1,242 @@
-import { closeToast, Icon, Image, showLoadingToast, showToast, Uploader } from 'vant'
-import { defineComponent } from 'vue'
-import styles from './index.module.less'
-import ColCropper from '../o-cropper'
-import { useCustomFieldValue } from '@vant/use'
-import { postMessage } from '@/helpers/native-message'
-import umiRequest from 'umi-request'
-import iconUploader from '@common/images/icon_uploader.png'
-import request from '@/helpers/request'
-import { getOssUploadUrl, state } from '@/state'
-
-export default defineComponent({
-  name: 'col-upload',
-  props: {
-    modelValue: String,
-    tips: {
-      type: String,
-      default: '点击上传'
-    },
-    deletable: {
-      type: Boolean,
-      default: true
-    },
-    native: {
-      // 是否原生上传
-      type: Boolean,
-      default: false
-    },
-    cropper: {
-      // 是否进行裁切
-      type: Boolean,
-      default: false
-    },
-    options: {
-      // 裁切需要参数
-      type: Object,
-      default: {}
-    },
-    uploadSize: {
-      // 上传图片大小
-      type: Number,
-      default: 5
-    },
-    onUploadChange: {
-      type: Function,
-      default: (url: string) => {}
-    },
-    bucket: {
-      type: String,
-      default: 'daya'
-    }
-  },
-  methods: {
-    nativeUpload() {
-      postMessage(
-        {
-          api: 'chooseFile',
-          content: { type: 'img', max: 1, bucket: this.bucket }
-        },
-        (res: any) => {
-          console.log(res, 'fileUrl')
-          this.$emit('update:modelValue', res.fileUrl)
-        }
-      )
-    },
-    beforeRead(file: any) {
-      console.log(file, 'beforeRead')
-      const isLt2M = file.size / 1024 / 1024 < this.uploadSize
-      if (!isLt2M) {
-        showToast(`上传文件大小不能超过 ${this.uploadSize}MB`)
-        return false
-      }
-      return true
-    },
-    beforeDelete(file: any, detail: { index: any }) {
-      // this.dataModel.splice(detail.index, 1)
-      return true
-    },
-    async afterRead(file: any, detail: any) {
-      try {
-        file.status = 'uploading'
-        file.message = '上传中...'
-        await this.uploadFile(file.file)
-      } catch (error) {
-        //
-        console.log(error, '2323')
-
-        closeToast()
-      }
-    },
-    onClose(e: any) {
-      this.$emit('update:modelValue', null)
-      this.onUploadChange()
-      e.stopPropagation()
-    },
-    async getFile(file: any) {
-      try {
-        await this.uploadFile(file)
-      } catch {
-        //
-      }
-    },
-    async uploadFile(file: any) {
-      // 上传文件
-      try {
-        // 获取签名
-        if (state.platformType === 'SCHOOL') {
-          state.platformApi = '/api-school'
-        } else if (state.platformType === 'TEACHER') {
-          state.platformApi = '/api-teacher'
-        } else if (state.platformType === 'STUDENT') {
-          state.platformApi = '/api-student'
-        }
-        const signUrl = state.platformApi + '/open/getUploadSign'
-        const tempName = file.name || ''
-        const fileName = tempName && tempName.replace(/ /gi, '_')
-        const key = new Date().getTime() + fileName
-        console.log(file)
-
-        const res = await request.post(signUrl, {
-          data: {
-            filename: fileName,
-            bucketName: this.bucket,
-            postData: {
-              filename: fileName,
-              acl: 'public-read',
-              key: key,
-              unknowValueField: []
-            }
-          }
-        })
-        showLoadingToast({
-          message: '加载中...',
-          forbidClick: true,
-          loadingType: 'spinner',
-          duration: 0
-        })
-        const obj = {
-          policy: res.data.policy,
-          signature: res.data.signature,
-          key: key,
-          KSSAccessKeyId: res.data.kssAccessKeyId,
-          acl: 'public-read',
-          name: fileName
-        }
-        const formData = new FormData()
-        for (const key in obj) {
-          formData.append(key, obj[key])
-        }
-        formData.append('file', file, fileName)
-        await umiRequest(getOssUploadUrl(this.bucket), {
-          method: 'POST',
-          data: formData
-        })
-        console.log(getOssUploadUrl(this.bucket) + key)
-        const uploadUrl = getOssUploadUrl(this.bucket) + key
-        closeToast()
-        this.$emit('update:modelValue', uploadUrl)
-        this.onUploadChange(uploadUrl)
-      } catch (error) {
-        console.log(error, 'uploadFile')
-      }
-    }
-  },
-  render() {
-    useCustomFieldValue(() => this.modelValue)
-    return (
-      <div class={styles['uploader-section']}>
-        {this.modelValue && this.deletable ? (
-          <Icon name="cross" onClick={this.onClose} class={styles['img-close']} />
-        ) : null}
-        {this.cropper && !this.native ? (
-          <div class={styles['col-uploader']}>
-            {this.modelValue ? (
-              <Image fit="cover" position="center" class={styles.uploadImg} src={this.modelValue} />
-            ) : (
-              <div class={styles.uploader}>
-                <Icon name={iconUploader} size="32" />
-                <p class={styles.uploaderText}>{this.tips}</p>
-              </div>
-            )}
-            <ColCropper option={this.options} getFile={this.getFile} />
-          </div>
-        ) : this.native ? (
-          <div
-            style={{
-              display: 'flex',
-              alignItems: 'center',
-              justifyContent: 'center',
-              height: '100%'
-            }}
-            onClick={this.nativeUpload}
-          >
-            {this.modelValue ? (
-              <Image fit="cover" position="center" class={styles.uploadImg} src={this.modelValue} />
-            ) : (
-              <div class={styles.uploader}>
-                <Icon name={iconUploader} size="32" />
-                <p class={styles.uploaderText}>{this.tips}</p>
-              </div>
-            )}
-          </div>
-        ) : (
-          <Uploader
-            afterRead={this.afterRead}
-            beforeRead={this.beforeRead}
-            beforeDelete={this.beforeDelete}
-            v-slots={{
-              default: () =>
-                this.modelValue ? (
-                  <Image
-                    fit="cover"
-                    position="center"
-                    class={styles.uploadImg}
-                    src={this.modelValue}
-                  />
-                ) : (
-                  <div class={styles.uploader}>
-                    <Icon name={iconUploader} size="32" />
-                    <p class={styles.uploaderText}>{this.tips}</p>
-                  </div>
-                )
-            }}
-          />
-        )}
-      </div>
-    )
-  }
-})
+import {
+  closeToast,
+  Icon,
+  Image,
+  showLoadingToast,
+  showToast,
+  Uploader,
+  UploaderInstance
+} from 'vant'
+import { defineComponent, ref } from 'vue'
+import styles from './index.module.less'
+import ColCropper from '../o-cropper'
+import { useCustomFieldValue } from '@vant/use'
+import { postMessage } from '@/helpers/native-message'
+import umiRequest from 'umi-request'
+import iconUploader from '@common/images/icon_uploader.png'
+import request from '@/helpers/request'
+import { getOssUploadUrl, state } from '@/state'
+
+export default defineComponent({
+  name: 'col-upload',
+  props: {
+    modelValue: String,
+    tips: {
+      type: String,
+      default: '点击上传'
+    },
+    deletable: {
+      type: Boolean,
+      default: true
+    },
+    native: {
+      // 是否原生上传
+      type: Boolean,
+      default: false
+    },
+    cropper: {
+      // 是否进行裁切
+      type: Boolean,
+      default: false
+    },
+    options: {
+      // 裁切需要参数
+      type: Object,
+      default: {}
+    },
+    uploadSize: {
+      // 上传图片大小
+      type: Number,
+      default: 5
+    },
+    onUploadChange: {
+      type: Function,
+      default: (url: string) => {}
+    },
+    bucket: {
+      type: String,
+      default: 'daya'
+    }
+  },
+  methods: {
+    nativeUpload() {
+      postMessage(
+        {
+          api: 'chooseFile',
+          content: { type: 'img', max: 1, bucket: this.bucket }
+        },
+        (res: any) => {
+          console.log(res, 'fileUrl')
+          this.$emit('update:modelValue', res.fileUrl)
+        }
+      )
+    },
+    beforeRead(file: any) {
+      console.log(file, 'beforeRead')
+      const isLt2M = file.size / 1024 / 1024 < this.uploadSize
+      if (!isLt2M) {
+        showToast(`上传文件大小不能超过 ${this.uploadSize}MB`)
+        return false
+      }
+      return true
+    },
+    beforeDelete(file: any, detail: { index: any }) {
+      // this.dataModel.splice(detail.index, 1)
+      return true
+    },
+    async afterRead(file: any, detail: any) {
+      try {
+        file.status = 'uploading'
+        file.message = '上传中...'
+        await this.uploadFile(file.file)
+      } catch (error) {
+        //
+        console.log(error, '2323')
+
+        closeToast()
+      }
+    },
+    onClose(e: any) {
+      this.$emit('update:modelValue', null)
+      this.onUploadChange()
+      e.stopPropagation()
+    },
+    async getFile(file: any) {
+      try {
+        await this.uploadFile(file)
+      } catch {
+        //
+      }
+    },
+    async uploadFile(file: any) {
+      // 上传文件
+      try {
+        // 获取签名
+        if (state.platformType === 'SCHOOL') {
+          state.platformApi = '/api-school'
+        } else if (state.platformType === 'TEACHER') {
+          state.platformApi = '/api-teacher'
+        } else if (state.platformType === 'STUDENT') {
+          state.platformApi = '/api-student'
+        }
+        const signUrl = state.platformApi + '/open/getUploadSign'
+        const tempName = file.name || ''
+        const fileName = tempName && tempName.replace(/ /gi, '_')
+        const key = new Date().getTime() + fileName
+        console.log(file)
+
+        const res = await request.post(signUrl, {
+          data: {
+            filename: fileName,
+            bucketName: this.bucket,
+            postData: {
+              filename: fileName,
+              acl: 'public-read',
+              key: key,
+              unknowValueField: []
+            }
+          }
+        })
+        showLoadingToast({
+          message: '加载中...',
+          forbidClick: true,
+          loadingType: 'spinner',
+          duration: 0
+        })
+        const obj = {
+          policy: res.data.policy,
+          signature: res.data.signature,
+          key: key,
+          KSSAccessKeyId: res.data.kssAccessKeyId,
+          acl: 'public-read',
+          name: fileName
+        }
+        const formData = new FormData()
+        for (const key in obj) {
+          formData.append(key, obj[key])
+        }
+        formData.append('file', file, fileName)
+        await umiRequest(getOssUploadUrl(this.bucket), {
+          method: 'POST',
+          data: formData
+        })
+        console.log(getOssUploadUrl(this.bucket) + key)
+        const uploadUrl = getOssUploadUrl(this.bucket) + key
+        closeToast()
+        this.$emit('update:modelValue', uploadUrl)
+        this.onUploadChange(uploadUrl)
+      } catch (error) {
+        console.log(error, 'uploadFile')
+      }
+    },
+    startUpload() {
+      ;(this as any).$refs.uploaderRef?.startChiose()
+    }
+  },
+
+  render() {
+    useCustomFieldValue(() => this.modelValue)
+
+    return (
+      <div class={styles['uploader-section']}>
+        {this.modelValue && this.deletable ? (
+          <Icon name="cross" onClick={this.onClose} class={styles['img-close']} />
+        ) : null}
+        {this.cropper && !this.native ? (
+          <div class={styles['col-uploader']}>
+            {this.modelValue ? (
+              <Image fit="cover" position="center" class={styles.uploadImg} src={this.modelValue} />
+            ) : (
+              <div class={styles.uploader}>
+                <Icon name={iconUploader} size="32" />
+                <p class={styles.uploaderText}>{this.tips}</p>
+              </div>
+            )}
+            <ColCropper ref="uploaderRef" option={this.options} getFile={this.getFile} />
+          </div>
+        ) : this.native ? (
+          <div
+            style={{
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'center',
+              height: '100%'
+            }}
+            onClick={this.nativeUpload}
+          >
+            {this.modelValue ? (
+              <Image fit="cover" position="center" class={styles.uploadImg} src={this.modelValue} />
+            ) : (
+              <div class={styles.uploader}>
+                <Icon name={iconUploader} size="32" />
+                <p class={styles.uploaderText}>{this.tips}</p>
+              </div>
+            )}
+          </div>
+        ) : (
+          <Uploader
+            afterRead={this.afterRead}
+            beforeRead={this.beforeRead}
+            beforeDelete={this.beforeDelete}
+            v-slots={{
+              default: () =>
+                this.modelValue ? (
+                  <Image
+                    fit="cover"
+                    position="center"
+                    class={styles.uploadImg}
+                    src={this.modelValue}
+                  />
+                ) : (
+                  <div class={styles.uploader}>
+                    <Icon name={iconUploader} size="32" />
+                    <p class={styles.uploaderText}>{this.tips}</p>
+                  </div>
+                )
+            }}
+          />
+        )}
+      </div>
+    )
+  }
+})

+ 9 - 0
src/router/routes-school.ts

@@ -190,6 +190,15 @@ export default [
         }
       },
       {
+        path: '/teacher-attendDetail',
+        name: 'teacher-attendDetail',
+        component: () => import('@/school/attendance/components/teacher-attendDetail'),
+        meta: {
+          title: '考勤详情'
+        }
+      },
+      //
+      {
         path: '/ranking-list',
         name: 'ranking-list',
         component: () => import('@/school/ranking-list/index'),

+ 9 - 1
src/router/routes-teacher.ts

@@ -55,6 +55,14 @@ export default [
         }
       },
       {
+        path: '/teacher-attendDetail',
+        name: 'teacher-attendDetail',
+        component: () => import('@/school/attendance/components/teacher-attendDetail'),
+        meta: {
+          title: '考勤详情'
+        }
+      },
+      {
         path: '/attendance-rule',
         name: 'attendance-rule',
         component: () => import('@/views/attendance-rule/index'),
@@ -69,7 +77,7 @@ export default [
         meta: {
           title: '教学课件'
         }
-      },
+      }
     ]
   },
   ...rootRouter,

+ 231 - 0
src/school/attendance/components/teacher-attendDetail.module.less

@@ -0,0 +1,231 @@
+.itemWrap {
+  margin: 12px 13px 12px;
+  font-size: 14px;
+  .itemWrapTopCard {
+    padding: 12px 15px 15px;
+    border-radius: 10px;
+    background-color: #fff;
+    margin-bottom: 12px;
+    .itemWrapTop {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      justify-content: space-between;
+      padding-bottom: 12px;
+      border-bottom: 1px solid #f2f2f2;
+      .itemWrapTopLeft {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        .clockWrap {
+          width: 18px;
+          height: 18px;
+          margin-right: 6px;
+          img {
+            width: 100%;
+            height: 100%;
+          }
+        }
+        .leftTimer {
+          font-size: 14px;
+          font-weight: 500;
+          color: #333333;
+          line-height: 20px;
+        }
+      }
+      .itemWrapTopRight {
+        font-size: 12px;
+        color: #777;
+      }
+    }
+  }
+  .itemWrapBottom {
+    padding-top: 15px;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+    .msgIcon {
+      width: 24px;
+      height: 24px;
+      img {
+        width: 100%;
+      }
+    }
+    .courseInfo {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+      padding-bottom: 15px;
+      .headImgs {
+        width: 42px;
+        height: 42px;
+        border-radius: 50%;
+        overflow: hidden;
+        margin-right: 12px;
+      }
+      .infoMsg {
+        .infoMsgMain {
+          font-size: 16px;
+          font-weight: 600;
+          color: #333333;
+          line-height: 22px;
+        }
+        .infoMsgSub {
+          font-size: 12px;
+          font-weight: 400;
+          color: #777777;
+          line-height: 17px;
+        }
+      }
+    }
+  }
+  .attBackInfo {
+    margin-top: 12px;
+  }
+  .attInfo {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: space-between;
+    .attInfoDot {
+      text-align: left;
+      width: 100%;
+      padding: 12px;
+      .attInfoDotTitle {
+        width: 100%;
+        display: flex;
+        flex-direction: row;
+        margin-bottom: 7px;
+        align-items: center;
+        justify-content: space-between;
+        img {
+          width: 18px;
+          height: 18px;
+        }
+      }
+      .signTime {
+        font-size: 14px;
+        font-weight: 500;
+        color: #333333;
+        line-height: 28px;
+        height: 31px;
+        line-height: 31px;
+        span {
+          border-radius: 10px;
+          font-size: 18px;
+          color: #333;
+          font-weight: 600;
+        }
+      }
+      .attRang {
+        height: 31px;
+        line-height: 31px;
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        font-size: 14px;
+        .locP {
+          margin-left: 5px;
+          font-size: 12px;
+          font-weight: 500;
+        }
+      }
+    }
+  }
+  .passWrap {
+    background-color: #ddecff;
+  }
+  .goWrap {
+    background-color: #ffe1e1;
+  }
+  .error {
+    color: #f44541;
+  }
+  .pass {
+    color: #4493f6;
+  }
+  .passWrap,
+  .goWrap {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    border-radius: 10px;
+    justify-content: space-between;
+    text-align: center;
+    width: 168px;
+    .itemBottomMain {
+      font-size: 30px;
+      font-weight: bold;
+      color: #333333;
+      line-height: 35px;
+      margin-bottom: 2px;
+    }
+    .itemBottomSub {
+      font-size: 14px;
+      font-weight: 400;
+      color: #333333;
+      line-height: 20px;
+    }
+  }
+}
+
+:global {
+  .bottomSheet {
+    .van-action-sheet__description {
+      padding: 0 !important;
+      &::after {
+        border: none !important;
+      }
+    }
+  }
+}
+.bottomTitle {
+  padding: 15px 15px 20px;
+  text-align: left;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  height: 22px;
+  font-size: 16px;
+  font-family: PingFangSC-Medium, PingFang SC;
+  font-weight: 500;
+  color: #333333;
+  line-height: 22px;
+  .bottomTitleLeft {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    span {
+      width: 4px;
+      height: 12px;
+      background: #ff8057;
+      border-radius: 2px;
+      display: inline-block;
+      margin-right: 6px;
+    }
+  }
+}
+.bottomConent {
+  padding: 20px 15px 57px;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  .bottomImgWrap {
+    width: 47px;
+    height: 47px;
+    margin-bottom: 6px;
+    img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+  .bottomConentLeft,
+  .bottomConentRight {
+    width: 50%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+  }
+}

+ 300 - 0
src/school/attendance/components/teacher-attendDetail.tsx

@@ -0,0 +1,300 @@
+import { defineComponent, reactive, ref, onMounted } from 'vue'
+import styles from './teacher-attendDetail.module.less'
+import clockIcon from '../images/clock-icon.png'
+import errorIcon from '../images/error-icon.png'
+import successIcon from '../images/success-icon.png'
+import defaultIcon from '@/school/images/default-icon.png'
+import sendmsgIcon from '@/school/images/sendmsg-icon.png'
+import phoneIcon from '@/school/images/phone-icon.png'
+import OHeader from '@/components/o-header'
+import msgIcon from '@/school/images/msg-icon.png'
+import { Icon, ActionSheet, showToast } from 'vant'
+import dayjs from 'dayjs'
+import { useRoute, useRouter } from 'vue-router'
+import { state as globalState } from '@/state'
+import request from '@/helpers/request'
+export default defineComponent({
+  props: ['item'],
+  name: 'teacher-attendDetail',
+  setup(props) {
+    const router = useRouter()
+    const route = useRoute()
+    const gotoStudentDetail = () => {
+      // router.push({ path: '/student-att-day', query: { time: teacherAttInfo.value.time } })
+    }
+    const teacherAttInfo = ref({} as any)
+    const platformApi = ref(globalState.platformApi)
+    const showContact = ref(false)
+    const startContact = () => {
+      showContact.value = true
+    }
+    const closeSheet = () => {
+      showContact.value = false
+    }
+    const postMsg = async () => {
+      try {
+        await postMessage({
+          api: 'joinChatGroup',
+          content: {
+            type: 'single', // single 单人 multi 多人
+            id: teacherAttInfo.value.imUserId
+          }
+        })
+        closeSheet()
+      } catch (e) {
+        showToast('发起聊天失败')
+        closeSheet()
+      }
+    }
+
+    const callPhone = async () => {
+      try {
+        await postMessage({
+          api: 'callPhone',
+          content: {
+            phone: teacherAttInfo.value.phone
+          }
+        })
+        closeSheet()
+      } catch (e) {
+        showToast('发起聊天失败')
+        closeSheet()
+      }
+    }
+    const getCourseAtt = async () => {
+      try {
+        const res = await request.post(
+          `${platformApi.value}/courseSchedule/teacherAttendance/course/${route.query.courseScheduleId}`,
+          {}
+        )
+        teacherAttInfo.value = res.data
+      } catch (e) {
+        console.log(e)
+      }
+    }
+    onMounted(() => {
+      getCourseAtt()
+    })
+    return () => (
+      <>
+        <OHeader></OHeader>
+        <div class={styles.itemWrap} onClick={gotoStudentDetail}>
+          <div class={styles.itemWrapTopCard}>
+            <div class={styles.itemWrapTop}>
+              <div class={styles.itemWrapTopLeft}>
+                <div class={styles.clockWrap}>
+                  <img src={clockIcon} alt="" />
+                </div>
+                <p class={styles.leftTimer}>
+                  {dayjs(teacherAttInfo.value?.startTime).format('YYYY-MM-DD HH:mm')}
+                  {'-'}
+                  {dayjs(teacherAttInfo.value?.endTime).format('HH:mm')}
+                </p>
+              </div>
+              <div class={styles.itemWrapTopRight}>{/* <Icon name="arrow"></Icon> */}</div>
+            </div>
+            <div class={styles.itemWrapBottom}>
+              <div class={styles.courseInfo}>
+                <img
+                  class={styles.headImgs}
+                  src={
+                    teacherAttInfo.value.teacherAvatar
+                      ? teacherAttInfo.value.teacherAvatar
+                      : defaultIcon
+                  }
+                  alt=""
+                />
+                <div class={styles.infoMsg}>
+                  <p class={styles.infoMsgMain}>
+                    {teacherAttInfo.value?.classGroupName}-{teacherAttInfo.value?.teacherName}
+                  </p>
+                  <p class={styles.infoMsgSub}>{teacherAttInfo.value?.orchestraName}</p>
+                </div>
+              </div>
+              <div
+                class={styles.msgIcon}
+                onClick={(e: any) => {
+                  e.stopPropagation()
+                  e.preventDefault()
+                  startContact()
+                }}
+              >
+                <img src={msgIcon} alt="" />
+              </div>
+            </div>
+          </div>
+
+          <div class={styles.attInfo}>
+            <div
+              class={
+                teacherAttInfo.value?.signInStatus === 'NORMAL' ? styles.passWrap : styles.goWrap
+              }
+            >
+              <div class={styles.attInfoDot}>
+                <div class={styles.attInfoDotTitle}>
+                  <span>
+                    上课时间 <span>{dayjs(teacherAttInfo.value?.startTime).format('HH:mm')}</span>
+                  </span>
+                  <img
+                    src={teacherAttInfo.value?.signInStatus === 'NORMAL' ? successIcon : errorIcon}
+                    alt=""
+                  />
+                </div>
+                <p class={styles.signTime}>
+                  签到时间
+                  <span>
+                    {teacherAttInfo.value?.signInTime
+                      ? dayjs(teacherAttInfo.value?.signInTime).format('HH:mm:ss')
+                      : '未签到'}
+                  </span>
+                </p>
+              </div>
+            </div>
+            <div
+              class={
+                teacherAttInfo.value?.signInLongitudeLatitudeStatus === 'NORMAL'
+                  ? styles.passWrap
+                  : styles.goWrap
+              }
+            >
+              <div class={styles.attInfoDot}>
+                <div class={styles.attInfoDotTitle}>
+                  <span>签到定位</span>
+                  <img
+                    src={
+                      teacherAttInfo.value?.signInLongitudeLatitudeStatus === 'NORMAL'
+                        ? successIcon
+                        : errorIcon
+                    }
+                    alt=""
+                  />
+                </div>
+                <div class={styles.attRang}>
+                  <p>考勤范围内</p>
+                  <p
+                    class={[
+                      styles.locP,
+                      teacherAttInfo.value?.signInLongitudeLatitudeStatus === 'NORMAL'
+                        ? styles.pass
+                        : styles.error
+                    ]}
+                  >
+                    查看定位 <Icon name="arrow" class={styles.arrow}></Icon>
+                  </p>
+                </div>
+              </div>
+            </div>
+          </div>
+          <div class={[styles.attInfo, styles.attBackInfo]}>
+            <div
+              class={
+                teacherAttInfo.value?.signInStatus === 'NORMAL' ? styles.passWrap : styles.goWrap
+              }
+            >
+              <div class={styles.attInfoDot}>
+                <div class={styles.attInfoDotTitle}>
+                  <p>
+                    下课时间<span>{dayjs(teacherAttInfo.value?.endTime).format('HH:mm')}</span>
+                  </p>
+                  <img
+                    src={teacherAttInfo.value?.signInStatus === 'NORMAL' ? successIcon : errorIcon}
+                    alt=""
+                  />
+                </div>
+                <p class={styles.signTime}>
+                  签退时间
+                  <span>
+                    {teacherAttInfo.value?.signOutTime
+                      ? dayjs(teacherAttInfo.value?.signOutTime).format('HH:mm:ss')
+                      : '未签退'}
+                  </span>
+                </p>
+              </div>
+            </div>
+            <div
+              class={
+                teacherAttInfo.value?.signOutStatus === 'NORMAL' ? styles.passWrap : styles.goWrap
+              }
+            >
+              <div class={styles.attInfoDot}>
+                <div class={styles.attInfoDotTitle}>
+                  <span>签退定位</span>
+                  <img
+                    src={teacherAttInfo.value?.signOutStatus === 'NORMAL' ? successIcon : errorIcon}
+                    alt=""
+                  />
+                </div>
+                <div class={styles.attRang}>
+                  <p>考勤范围内</p>
+                  <p
+                    class={[
+                      styles.locP,
+                      teacherAttInfo.value?.signInLongitudeLatitudeStatus === 'NORMAL'
+                        ? styles.pass
+                        : styles.error
+                    ]}
+                  >
+                    查看定位 <Icon name="arrow" class={styles.arrow}></Icon>
+                  </p>
+                </div>
+              </div>
+            </div>
+          </div>
+          <ActionSheet
+            class="bottomSheet"
+            v-model:show={showContact.value}
+            v-slots={{
+              description: () => (
+                <div class={styles.bottomTitle}>
+                  <div class={styles.bottomTitleLeft}>
+                    <span></span>
+                    <p>联系方式</p>
+                  </div>
+                  <div
+                    class={styles.bottomTitleRight}
+                    onClick={(e: any) => {
+                      e.stopPropagation()
+                      e.preventDefault()
+                      closeSheet()
+                    }}
+                  >
+                    <Icon class={styles.cross} name="cross"></Icon>
+                  </div>
+                </div>
+              )
+            }}
+          >
+            <div class={styles.bottomConent}>
+              <div
+                class={styles.bottomConentLeft}
+                onClick={(e: any) => {
+                  e.stopPropagation()
+                  e.preventDefault()
+                  postMsg()
+                }}
+              >
+                <div class={styles.bottomImgWrap}>
+                  <img src={sendmsgIcon} alt="" />
+                </div>
+                <p>发送消息</p>
+              </div>
+              <div
+                class={styles.bottomConentRight}
+                onClick={(e: any) => {
+                  e.stopPropagation()
+                  e.preventDefault()
+                  callPhone()
+                }}
+              >
+                <div class={styles.bottomImgWrap}>
+                  <img src={phoneIcon} alt="" />
+                </div>
+                <p>拨打电话</p>
+              </div>
+            </div>
+          </ActionSheet>
+        </div>
+      </>
+    )
+  }
+})

+ 2 - 0
src/school/attendance/modals/teacherAtt-item.module.less

@@ -86,6 +86,8 @@
           }
         }
         .signTime {
+          height: 31px;
+          line-height: 31px;
           font-size: 20px;
           font-weight: 600;
           color: #333333;

+ 6 - 3
src/school/attendance/modals/teacherAtt-item.tsx

@@ -12,12 +12,15 @@ export default defineComponent({
   name: 'teacherAtt-item',
   setup(props) {
     const router = useRouter()
-    const gotoStudentDetail = () => {
-      // router.push({ path: '/student-att-day', query: { time: props.item.time } })
+    const gotoTeacherDetail = () => {
+      router.push({
+        path: '/teacher-attendDetail',
+        query: { courseScheduleId: props.item.courseScheduleId }
+      })
     }
     return () => (
       <>
-        <div class={styles.itemWrap} onClick={gotoStudentDetail}>
+        <div class={styles.itemWrap} onClick={gotoTeacherDetail}>
           <div class={styles.itemWrapTop}>
             <div class={styles.itemWrapTopLeft}>
               <div class={styles.clockWrap}>

+ 88 - 41
src/school/school-detail/eidt-school.tsx

@@ -1,12 +1,15 @@
 import OHeader from '@/components/o-header'
 import OSticky from '@/components/o-sticky'
-import { Form, Field, CellGroup, showToast, Icon, Popup, Picker, Button } from 'vant'
+import { Form, Field, CellGroup, showToast, Icon, Popup, Picker, Button, Image } from 'vant'
 import { defineComponent, reactive, ref, onMounted } from 'vue'
 import styles from './index.module.less'
 import { useRouter } from 'vue-router'
 import { state as globalState } from '@/state'
+import logoIcon from './images/logo.png'
+import locIcon from './images/loc-icon.png'
 import request from '@/helpers/request'
 import { areas } from '@/helpers/area'
+import OUpload from '@/components/o-upload'
 
 export default defineComponent({
   name: 'school-detail',
@@ -20,10 +23,13 @@ export default defineComponent({
       showProvince: ''
     })
     const forms = reactive({
-      provinceCode: '',
-      cityCode: '',
-      address: ''
+      address: '',
+      logo: '',
+      email: '',
+      emergencyContact: ''
     })
+    const schoolImageRef = ref()
+    console.log(schoolImageRef.value)
     const getSchoolDetail = async () => {
       const schoolId = (globalState.user.data.schoolInfos || [])
         .map((item) => {
@@ -34,24 +40,26 @@ export default defineComponent({
         const { data } = await request.get(`/api-school/school/detail/${schoolId}`, {})
         state.info = data
         state.showProvince = data.provinceName + data.cityName
-        forms.provinceCode = data.provinceCode
-        forms.cityCode = data.cityCode
+
         forms.address = data.address
+        forms.logo = data.logo
+        forms.email = data.email
+        forms.emergencyContact = data.emergencyContact
       } catch (e: any) {
         showToast(e.message)
       }
     }
 
     const submitInfo = async () => {
-      const schoolId = (globalState.user.data.schoolInfos || [])
-        .map((item) => {
-          return item.id
-        })
-        .join(',')
-      if (!forms.cityCode || !forms.provinceCode) {
-        showToast('请选择省市')
-        return
-      }
+      // const schoolId = (globalState.user.data.schoolInfos || [])
+      //   .map((item) => {
+      //     return item.id
+      //   })
+      //   .join(',')
+      // if (!forms.cityCode || !forms.provinceCode) {
+      //   showToast('请选择省市')
+      //   return
+      // }
       if (!forms.address) {
         showToast('请输入详细地址')
         return
@@ -59,8 +67,7 @@ export default defineComponent({
       try {
         const { data } = await request.post('/api-school/school/update', {
           data: {
-            ...forms,
-            schoolId
+            ...forms
           }
         })
         showToast('修改成功')
@@ -75,14 +82,16 @@ export default defineComponent({
     onMounted(() => {
       getSchoolDetail()
     })
-
-    const onConfirmArea = (val) => {
-      console.log(val)
-      state.showProvince = val.selectedOptions[0].name + val.selectedOptions[1].name
-      forms.provinceCode = val.selectedOptions[0].code
-      forms.cityCode = val.selectedOptions[1].code
-      state.showArea = false
+    const setSchoolIcon = () => {
+      schoolImageRef.value.startUpload()
     }
+    // const onConfirmArea = (val) => {
+    //   console.log(val)
+    //   state.showProvince = val.selectedOptions[0].name + val.selectedOptions[1].name
+    //   forms.provinceCode = val.selectedOptions[0].code
+    //   forms.cityCode = val.selectedOptions[1].code
+    //   state.showArea = false
+    // }
 
     onMounted(() => {
       // console.log(areas)
@@ -111,40 +120,78 @@ export default defineComponent({
           <OHeader></OHeader>
           <div class={styles.eidtWrap}>
             <CellGroup inset>
-              <Field
+              <div class={styles.schoolWrap} onClick={setSchoolIcon}>
+                <Image
+                  width={50}
+                  height={50}
+                  fix="cover"
+                  class={styles.schoolICon}
+                  src={forms.logo ? forms.logo : logoIcon}
+                ></Image>
+                <div class={styles.schoolBtnWrap}>
+                  <p>修改校徽</p>
+                  <Icon name="arrow" size="20px" color="#aaa"></Icon>
+                </div>
+              </div>
+            </CellGroup>
+          </div>
+          <div class={styles.eidtWrap}>
+            <CellGroup inset>
+              {/* <Field
                 is-link
                 readonly
                 v-model={state.showProvince}
                 name="area"
                 label="地区选择"
                 placeholder="点击选择省市"
-                onClick={() => (state.showArea = true)}
-              ></Field>
+                disabled
+              ></Field> */}
+              {/*    onClick={() => (state.showArea = true)} */}
               <Field
-                type="textarea"
+                label-align="top"
                 rows={3}
                 v-model={forms.address}
-                label="详细地址"
                 maxlength={50}
-                placeholder="小区楼栋/乡村名称"
-              ></Field>
+                placeholder="请选择地址"
+                disabled
+              >
+                {{
+                  extra: () => <Image width={19} height={18} src={locIcon}></Image>,
+                  label: () => <p class={styles.addP}>地址</p>
+                }}
+              </Field>
+              <Field
+                label-align="top"
+                rows={3}
+                v-model={forms.email}
+                maxlength={50}
+                placeholder="邮箱"
+              >
+                {{ label: () => <p class={styles.addP}>邮箱</p> }}
+              </Field>
+              <Field
+                label-align="top"
+                rows={3}
+                v-model={forms.emergencyContact}
+                maxlength={50}
+                placeholder="负责人"
+              >
+                {{ label: () => <p class={styles.addP}>负责人</p> }}
+              </Field>
             </CellGroup>
+            <OUpload
+              class={styles.upload}
+              cropper
+              v-model:modelValue={forms.logo}
+              ref={schoolImageRef}
+            />
           </div>
           <div class={styles.wall}></div>
           <div class={styles.bottomWrap}>
             <Button type="primary" size="large" onClick={submitInfo}>
-              确 认
+              确认修改
             </Button>
           </div>
-          <Popup v-model:show={state.showArea} position="bottom">
-            <Picker
-              showToolbar
-              columnsFieldNames={{ text: 'name', value: 'code', children: 'areas' }}
-              onConfirm={onConfirmArea}
-              columns={state.columns}
-              onCancel={() => (state.showArea = false)}
-            ></Picker>
-          </Popup>
         </div>
       </>
     )

BIN
src/school/school-detail/images/loc-icon.png


+ 38 - 0
src/school/school-detail/index.module.less

@@ -89,6 +89,11 @@
 .schoolEidtWrap {
   .eidtWrap {
     margin: 12px 0 12px;
+    :global {
+      .van-cell {
+        padding: 18px 12px;
+      }
+    }
   }
 }
 .wall {
@@ -110,3 +115,36 @@
     }
   }
 }
+
+.addP {
+  font-size: 16px;
+  font-weight: 500;
+  color: #333333;
+  line-height: 22px;
+}
+.schoolWrap {
+  padding: 14px 12px;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  .schoolICon {
+    border-radius: 50%;
+    overflow: hidden;
+  }
+  .schoolBtnWrap {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    p {
+      font-size: 16px;
+      font-weight: 400;
+      color: #aaaaaa;
+      margin-right: 10px;
+    }
+  }
+}
+
+.upload {
+  visibility: hidden;
+}

+ 4 - 2
src/school/school-detail/index.tsx

@@ -86,7 +86,9 @@ export default defineComponent({
               background={state.heightV > state.scrollTop ? 'transparent' : '#fff'}
             >
               {{
-                right: () => <Icon style={{ fontSize: '24px' }} name={eidtIcon}></Icon>
+                right: () => (
+                  <Icon style={{ fontSize: '24px' }} name={eidtIcon} onClick={gotoEidt}></Icon>
+                )
               }}
             </OHeader>
           </OSticky>
@@ -104,7 +106,7 @@ export default defineComponent({
               </div>
             </div>
             <div class={styles.detailInfo}>
-              <p onClick={gotoEidt}>地址:{state.info.address}</p>
+              <p>地址:{state.info.address}</p>
               <p>学年制:{schoolSystem[state.info.schoolSystem]} </p>
               <p>邮箱:{state.info.email}</p>
               <p>管理老师:{state.info.educationalAdministrationUsername}</p>

+ 6 - 3
src/teacher/attendance/modals/teacherAtt-item.tsx

@@ -11,12 +11,15 @@ export default defineComponent({
   name: 'teacherAtt-item',
   setup(props) {
     const router = useRouter()
-    const gotoStudentDetail = () => {
-      // router.push({ path: '/student-att-day', query: { time: props.item.time } })
+    const gotoTeacherDetail = () => {
+      router.push({
+        path: '/teacher-attendDetail',
+        query: { courseScheduleId: props.item.courseScheduleId }
+      })
     }
     return () => (
       <>
-        <div class={styles.itemWrap} onClick={gotoStudentDetail}>
+        <div class={styles.itemWrap} onClick={gotoTeacherDetail}>
           <div class={styles.itemWrapTop}>
             <div class={styles.itemWrapTopLeft}>
               <div class={styles.clockWrap}>