lex 2 年之前
父節點
當前提交
f5e016e854
共有 28 個文件被更改,包括 2013 次插入108 次删除
  1. 230 0
      src/components/col-cropper copy/cropper.tsx
  2. 85 0
      src/components/col-cropper copy/index.module.less
  3. 272 0
      src/components/col-cropper copy/index.tsx
  4. 271 0
      src/components/col-cropper-1/cropper.tsx
  5. 85 0
      src/components/col-cropper-1/index.module.less
  6. 427 0
      src/components/col-cropper-1/index.tsx
  7. 1 1
      src/components/col-cropper/cropper.tsx
  8. 1 1
      src/components/col-cropper/index.tsx
  9. 13 1
      src/components/col-upload-video/index.module.less
  10. 65 21
      src/components/col-upload-video/index.tsx
  11. 5 0
      src/components/col-upload/index.tsx
  12. 0 7
      src/views/user-info/components/user-menu/index.tsx
  13. 1 1
      src/views/user-info/live-class/list.tsx
  14. 1 1
      src/views/user-info/music-class/list.tsx
  15. 9 0
      src/views/user-info/practice-setting/index.tsx
  16. 7 5
      src/views/user-info/video-class/index.tsx
  17. 16 2
      src/views/user-info/video-class/list.tsx
  18. 14 0
      src/views/user-info/video-operation/course-content/index.module.less
  19. 146 13
      src/views/user-info/video-operation/course-content/index.tsx
  20. 7 0
      src/views/user-info/video-operation/course-info/index.module.less
  21. 46 22
      src/views/user-info/video-operation/course-info/index.tsx
  22. 134 0
      src/views/user-info/video-operation/course-preview/index.module.less
  23. 133 0
      src/views/user-info/video-operation/course-preview/index.tsx
  24. 41 32
      src/views/user-info/video-operation/createState.tsx
  25. 二進制
      src/views/user-info/video-operation/images/icon_course_introduction.png
  26. 二進制
      src/views/user-info/video-operation/images/icon_course_list.png
  27. 二進制
      src/views/user-info/video-operation/images/icon_video_stop.png
  28. 3 1
      src/views/user-info/video-operation/index.tsx

+ 230 - 0
src/components/col-cropper copy/cropper.tsx

@@ -0,0 +1,230 @@
+import { defineComponent } from 'vue'
+import styles from './index.module.less'
+import { VueCropper } from 'vue-cropper'
+import umiRequest from 'umi-request'
+import 'vue-cropper/dist/index.css'
+import {
+  ElButton,
+  ElCol,
+  ElDialog,
+  ElIcon,
+  ElImage,
+  ElMessage,
+  ElRow
+} from 'element-plus'
+import { CirclePlus, Remove } from '@element-plus/icons-vue'
+import iconRate from '../col-upload/images/icon_rate.png'
+import request from '@/helpers/request'
+
+export default defineComponent({
+  name: 'cropper',
+  props: {
+    cropperNo: {
+      type: Function,
+      default: (data: any) => {}
+    },
+    cropperOk: {
+      type: Function,
+      default: (data: any) => {}
+    },
+    bucket: {
+      type: String,
+      default: 'daya'
+    }
+  },
+  data() {
+    return {
+      ossUploadUrl: 'https://ks3-cn-beijing.ksyuncs.com/' + this.bucket,
+      dataObj: {
+        policy: '',
+        signature: '',
+        key: '',
+        KSSAccessKeyId: '',
+        acl: 'public-read',
+        name: ''
+      },
+      visible: false,
+      img: null,
+      options: {
+        img: '', //裁剪图片的地址
+        autoCrop: true, //是否默认生成截图框
+        autoCropWidth: 180, //默认生成截图框宽度
+        autoCropHeight: 180, //默认生成截图框高度
+        fixedBox: true, //是否固定截图框大小 不允许改变
+        full: false,
+        enlarge: 1, // 是否按照截图框比例输出 默认为1
+        previewsCircle: true, //预览图是否是原圆形
+        centerBox: true,
+        outputType: 'png',
+        title: '修改头像',
+        name: null // 文件名称
+      },
+      previews: {} as any,
+      url: {
+        upload: '/sys/common/saveToImgByStr'
+      },
+      submitLoading: false
+    }
+  },
+  methods: {
+    edit(record: any) {
+      const { options } = this
+      this.visible = true
+      this.options = Object.assign({}, options, record)
+      console.log(this.options)
+    },
+    /**
+     * 取消截图
+     */
+    cancelHandel() {
+      this.visible = false
+      this.cropperNo()
+    },
+    /**
+     * 确认截图
+     */
+    okHandel() {
+      ;(this as any).$refs.cropperRef.getCropBlob(async data => {
+        // this.submitLoading = true
+        // context.emit('cropper-ok', data)
+        console.log(data, 'data')
+        this.visible = false
+        this.cropperOk(data, this.options)
+      })
+    },
+    //转成blob
+    blobToFile(Blob: any, fileName: any) {
+      //兼容IE
+      Blob.lastModifiedDate = new Date()
+      Blob.name = fileName
+      return Blob
+    },
+    base64ToFile(urlData: any, fileName: any) {
+      let arr = urlData.split(',')
+      let mime = arr[0].match(/:(.*?);/)[1]
+      let bytes = atob(arr[1]) // 解码base64
+      let n = bytes.length
+      let ia = new Uint8Array(n)
+      while (n--) {
+        ia[n] = bytes.charCodeAt(n)
+      }
+      return new File([ia], fileName, { type: mime })
+    },
+    //移动框的事件
+    realTime(data: any) {
+      this.previews = data
+    },
+    //图片缩放
+    changeScale(num: number) {
+      num = num || 1
+      ;(this as any).$refs.cropperRef.changeScale(num)
+    },
+    //向左旋转
+    rotateLeft() {
+      ;(this as any).$refs.cropperRef.rotateLeft()
+    },
+    //向右旋转
+    rotateRight() {
+      ;(this as any).$refs.cropperRef.rotateRight()
+    }
+  },
+  render() {
+    return (
+      <ElDialog
+        modelValue={this.visible}
+        onUpdate:modelValue={val => (this.visible = val)}
+        appendToBody
+        title={this.options.title}
+        closeOnClickModal={false}
+        width={'800px'}
+        v-slots={{
+          footer: () => (
+            <span class="dialog-footer !text-center block">
+              <ElButton
+                onClick={this.cancelHandel}
+                disabled={this.submitLoading}
+              >
+                取消
+              </ElButton>
+              <ElButton
+                type="primary"
+                onClick={this.okHandel}
+                loading={this.submitLoading}
+              >
+                保 存
+              </ElButton>
+            </span>
+          )
+        }}
+      >
+        <ElRow>
+          <ElCol xs={24} md={12} style={{ width: '350px' }}>
+            <VueCropper
+              ref="cropperRef"
+              img={this.options.img}
+              info={true}
+              autoCrop={this.options.autoCrop}
+              autoCropWidth={this.options.autoCropWidth}
+              full={this.options.full}
+              outputType={this.options.outputType}
+              autoCropHeight={this.options.autoCropHeight}
+              fixedBox={this.options.fixedBox}
+              enlarge={this.options.enlarge}
+              onRealTime={this.realTime}
+              style={{ height: '350px' }}
+            />
+            <div class="flex pt-2">
+              <div
+                onClick={() => {
+                  this.changeScale(1)
+                }}
+                class="mr-2 cursor-pointer"
+                title="放大"
+              >
+                <ElIcon size={30} color="#333">
+                  <CirclePlus />
+                </ElIcon>
+              </div>
+              <div
+                onClick={() => {
+                  this.changeScale(-1)
+                }}
+                class="mr-2 cursor-pointer"
+                title="缩小"
+              >
+                <ElIcon size={30} color="#333">
+                  <Remove />
+                </ElIcon>
+              </div>
+              <div
+                onClick={this.rotateRight}
+                title="向右旋转"
+                class="cursor-pointer"
+              >
+                <img src={iconRate} class="w-[30px] h-[30px]" />
+              </div>
+            </div>
+          </ElCol>
+          <ElCol xs={24} md={12} style={{ height: '350px' }}>
+            <div class={styles.previewImg}>
+              <span>预览图片</span>
+              <div
+                class={
+                  this.options.previewsCircle
+                    ? styles['avatar-upload-preview']
+                    : styles['avatar-upload-preview_range']
+                }
+                style={{
+                  width: this.options.autoCropWidth + 'px',
+                  height: this.options.autoCropHeight + 'px'
+                }}
+              >
+                <ElImage src={this.previews.url} style={this.previews.img} />
+              </div>
+            </div>
+          </ElCol>
+        </ElRow>
+      </ElDialog>
+    )
+  }
+})

+ 85 - 0
src/components/col-cropper copy/index.module.less

@@ -0,0 +1,85 @@
+.uploadSection {
+  width: 156px;
+  height: 106px;
+  background: #f8faf9;
+  border-radius: 4px;
+  border: 1px solid rgba(45, 199, 170, 0.26);
+  font-size: 14px;
+  color: #2dc7aa;
+  line-height: 20px;
+}
+
+.uploadClass {
+  height: 106px;
+  width: 100%;
+
+  :global {
+    .el-loading-spinner {
+      margin-top: -43px;
+      height: 106px;
+    }
+  }
+}
+
+.avatar-upload-preview_range,
+.avatar-upload-preview {
+  width: 180px;
+  height: 180px;
+  box-shadow: 0 0 4px #ccc;
+  overflow: hidden;
+  img {
+    background-color: #f7f7f7;
+    height: 100%;
+  }
+}
+.avatar-upload-preview_range {
+  border-radius: 0;
+}
+.previewImg {
+  padding-left: 50px;
+  padding-top: 10px;
+  & > span {
+    display: block;
+    color: #212121;
+    font-size: 16px;
+    padding-bottom: 15px;
+  }
+}
+.operation {
+  font-size: 24px;
+  display: flex;
+  align-items: center;
+  margin-top: 20px;
+  & > i {
+    margin-left: 12px;
+    cursor: pointer;
+  }
+  .icon-rate {
+    display: inline-block;
+    width: 20px;
+    height: 20px;
+    background: url('./images/icon_rate.png') no-repeat center;
+    background-size: contain;
+  }
+}
+.vue-cropper {
+  border-radius: 5px;
+  overflow: hidden;
+}
+:deep(.el-dialog) {
+  margin-bottom: 10vh;
+  .el-dialog__header {
+    // background: #363d55;
+    background: #fff;
+    padding: 15px 20px 15px;
+    .el-dialog__title {
+      color: #212121;
+    }
+    .el-dialog__headerbtn .el-dialog__close {
+      color: #212121;
+    }
+  }
+  .el-dialog__body {
+    padding-top: 0;
+  }
+}

+ 272 - 0
src/components/col-cropper copy/index.tsx

@@ -0,0 +1,272 @@
+import { ElImage, ElMessage, ElUpload } from 'element-plus'
+import { defineComponent } from 'vue'
+import styles from './index.module.less'
+import iconUpload from '../col-upload/images/icon_upload.png'
+import Cropper from './cropper'
+import request from '@/helpers/request'
+import umiRequest from 'umi-request'
+
+export default defineComponent({
+  name: 'col-cropper',
+  props: {
+    modelValue: {
+      type: String,
+      default: ''
+    },
+    options: {
+      // 裁切需要参数
+      type: Object,
+      default: {
+        autoCrop: true, //是否默认生成截图框
+        enlarge: 1, //  图片放大倍数
+        autoCropWidth: 200, //默认生成截图框宽度
+        autoCropHeight: 200, //默认生成截图框高度
+        fixedBox: true, //是否固定截图框大小 不允许改变
+        previewsCircle: true, //预览图是否是原圆形
+        title: '上传图片'
+      }
+    },
+    // 显示图片原始图片
+    showSize: {
+      type: Boolean,
+      default: false
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    bucket: {
+      type: String,
+      default: 'daya'
+    },
+    size: {
+      type: Number,
+      default: 5 // 默认5M
+    },
+    accept: {
+      type: String,
+      default: 'images/*'
+    },
+    tips: {
+      type: String,
+      default: '请上传图片'
+    },
+    extraTips: {
+      type: String,
+      default: '图片最大不能超过5MB'
+    },
+    cropUploadSuccess: {
+      type: Function,
+      default: (data: string) => {}
+    }
+  },
+  data() {
+    return {
+      isStopRun: false,
+      loading: false,
+      ossUploadUrl: 'https://ks3-cn-beijing.ksyuncs.com/' + this.bucket,
+      dataObj: {
+        policy: '',
+        signature: '',
+        key: '',
+        KSSAccessKeyId: '',
+        acl: 'public-read',
+        name: ''
+      },
+      data: null as any
+    }
+  },
+  methods: {
+    onDelete() {
+      // 删除图片
+      this.$emit('update:modelValue', '')
+    },
+
+    change(info: any) {
+      this.loading = true
+      const options = this.options
+      console.log(info)
+      this.getBase64(info.raw, (imageUrl: any) => {
+        const target = Object.assign({}, options, {
+          img: imageUrl,
+          name: info.raw.name // 上传文件名
+        })
+        ;(this as any).$refs.CropperModal.edit(target)
+      })
+    },
+    //从本地选择文件
+    async handleChange(options: any) {
+      // console.log(info, 1212)
+      // return
+      // if (this.isStopRun) {
+      //   return
+      // }
+      // this.loading = true
+      // const options = this.options
+      // this.getBase64(info.file, (imageUrl: any) => {
+      //   const target = Object.assign({}, options, {
+      //     img: imageUrl,
+      //     name: info.file.name // 上传文件名
+      //   })
+      //   ;(this as any).$refs.CropperModal.edit(target)
+      // })
+      // const options: any = this.options
+      const fileName =
+        (options.name ? options.name.split('.')[0] : +new Date()) + '.png'
+      try {
+        let key = new Date().getTime() + fileName
+        let obj = {
+          filename: fileName,
+          bucketName: this.bucket,
+          postData: {
+            filename: fileName,
+            acl: 'public-read',
+            key: key,
+            unknowValueField: []
+          }
+        }
+        const res = await request.post('/api-website/getUploadSign', {
+          data: obj
+        })
+        this.dataObj = {
+          policy: res.data.policy,
+          signature: res.data.signature,
+          key: key,
+          KSSAccessKeyId: res.data.kssAccessKeyId,
+          acl: 'public-read',
+          name: fileName
+        }
+
+        let formData = new FormData()
+        for (let key in this.dataObj) {
+          formData.append(key, this.dataObj[key])
+        }
+        formData.append('file', this.blobToFile(this.data, fileName), fileName)
+        await umiRequest(this.ossUploadUrl, {
+          method: 'POST',
+          data: formData
+        })
+        console.log(this.ossUploadUrl + '/' + key)
+        const uploadUrl = this.ossUploadUrl + '/' + key
+        // this.cropperOk(uploadUrl)
+        this.$emit('update:modelValue', uploadUrl)
+      } catch (err: any) {
+        ElMessage.error(err)
+      } finally {
+        // this.submitLoading = false
+        // this.cancelHandel()
+      }
+    },
+    //转成blob
+    blobToFile(Blob: any, fileName: any) {
+      //兼容IE
+      Blob.lastModifiedDate = new Date()
+      Blob.name = fileName
+      return Blob
+    },
+    // 上传之前 格式与大小校验
+    beforeUpload(file) {
+      this.isStopRun = false
+      var fileType = file.type
+      if (fileType.indexOf('image') < 0) {
+        ElMessage.warning('请上传图片')
+        this.isStopRun = true
+        return false
+      }
+      // const isJpgOrPng = this.acceptArray.includes(file.type)
+      // if (!isJpgOrPng) {
+      //   ElMessage.error('你上传图片格式不正确!')
+      //   this.isStopRun = true
+      // }
+      console.log(this.size)
+      const size = this.size || 0
+      const isLtSize = file.size < size * 1024 * 1024
+      if (!isLtSize) {
+        ElMessage.error('图片大小不能超过' + this.size + 'MB!')
+        this.isStopRun = true
+      }
+      return isLtSize
+    },
+    error() {
+      this.remove()
+      this.loading = false
+    },
+    remove() {
+      this.onDelete()
+    },
+    //获取服务器返回的地址
+    handleCropperSuccess(data: any, options: any) {
+      //将返回的数据回显
+      // this.loading = false
+      // console.log(data, 'success')
+      // this.$emit('update:modelValue', data)
+      // this.cropUploadSuccess(data)
+      // this.$emit('cropUploadSuccess', data)
+      // console.log(this.modelValue, 'modelValue')
+      this.data = data
+      // this.handleChange(options)
+      // console.log((this as any).$refs.uploadRef)
+      ;(this as any).$refs.uploadRef.submit()
+    },
+    // 取消上传
+    handleCropperClose() {
+      this.loading = false
+      this.remove()
+    },
+    getBase64(img, callback) {
+      const reader = new FileReader()
+      reader.addEventListener('load', () => callback(reader.result))
+      reader.readAsDataURL(img)
+    }
+  },
+  render() {
+    return (
+      <div class={[styles.colUpload, 'w-full']}>
+        <ElUpload
+          disabled={this.disabled}
+          showFileList={false}
+          accept={this.accept}
+          beforeUpload={this.beforeUpload}
+          // @ts-ignore
+          onChange={this.change}
+          httpRequest={this.handleChange}
+          // limit={1}
+          ref="uploadRef"
+        >
+          <div
+            ref="uploadDom"
+            class={[styles.uploadClass, 'w-full']}
+            style={{ height: '106px' }}
+          >
+            {this.modelValue ? (
+              <ElImage
+                src={this.modelValue}
+                fit="cover"
+                class={styles.uploadSection}
+              />
+            ) : (
+              <div
+                class={[
+                  styles.uploadSection,
+                  'flex items-center flex-col justify-center'
+                ]}
+              >
+                <img src={iconUpload} class="w-8 h-7 mb-3" />
+                <p>{this.tips}</p>
+              </div>
+            )}
+          </div>
+        </ElUpload>
+
+        <p class="text-3 text-[#999999] leading-6 pt-1">{this.extraTips}</p>
+
+        <Cropper
+          ref="CropperModal"
+          bucket={this.bucket}
+          cropperNo={this.handleCropperClose}
+          cropperOk={this.handleCropperSuccess}
+        />
+      </div>
+    )
+  }
+})

+ 271 - 0
src/components/col-cropper-1/cropper.tsx

@@ -0,0 +1,271 @@
+import { defineComponent } from 'vue'
+import styles from './index.module.less'
+import { VueCropper } from 'vue-cropper'
+import umiRequest from 'umi-request'
+import 'vue-cropper/dist/index.css'
+import {
+  ElButton,
+  ElCol,
+  ElDialog,
+  ElIcon,
+  ElImage,
+  ElMessage,
+  ElRow
+} from 'element-plus'
+import { CirclePlus, Remove } from '@element-plus/icons-vue'
+import iconRate from '../col-upload/images/icon_rate.png'
+import request from '@/helpers/request'
+
+export default defineComponent({
+  name: 'cropper',
+  props: {
+    cropperNo: {
+      type: Function,
+      default: (data: any) => {}
+    },
+    cropperOk: {
+      type: Function,
+      default: (data: any) => {}
+    },
+    bucket: {
+      type: String,
+      default: 'daya'
+    }
+  },
+  data() {
+    return {
+      ossUploadUrl: 'https://ks3-cn-beijing.ksyuncs.com/' + this.bucket,
+      dataObj: {
+        policy: '',
+        signature: '',
+        key: '',
+        KSSAccessKeyId: '',
+        acl: 'public-read',
+        name: ''
+      },
+      visible: false,
+      img: null,
+      options: {
+        img: '', //裁剪图片的地址
+        autoCrop: true, //是否默认生成截图框
+        autoCropWidth: 180, //默认生成截图框宽度
+        autoCropHeight: 180, //默认生成截图框高度
+        fixedBox: true, //是否固定截图框大小 不允许改变
+        full: false,
+        enlarge: 1, // 是否按照截图框比例输出 默认为1
+        previewsCircle: true, //预览图是否是原圆形
+        centerBox: true,
+        outputType: 'png',
+        title: '修改头像',
+        name: null // 文件名称
+      },
+      previews: {} as any,
+      url: {
+        upload: '/sys/common/saveToImgByStr'
+      },
+      submitLoading: false
+    }
+  },
+  methods: {
+    edit(record: any) {
+      const { options } = this
+      this.visible = true
+      this.options = Object.assign({}, options, record)
+      console.log(this.options)
+    },
+    /**
+     * 取消截图
+     */
+    cancelHandel() {
+      this.visible = false
+      this.cropperNo()
+    },
+    /**
+     * 确认截图
+     */
+    okHandel() {
+      ;(this as any).$refs.cropperRef.getCropBlob(async data => {
+        this.submitLoading = true
+        const options: any = this.options
+        const fileName =
+          (options.name ? options.name.split('.')[0] : +new Date()) + '.png'
+        try {
+          let key = new Date().getTime() + fileName
+          let obj = {
+            filename: fileName,
+            bucketName: this.bucket,
+            postData: {
+              filename: fileName,
+              acl: 'public-read',
+              key: key,
+              unknowValueField: []
+            }
+          }
+          const res = await request.post('/api-website/getUploadSign', {
+            data: obj
+          })
+          this.dataObj = {
+            policy: res.data.policy,
+            signature: res.data.signature,
+            key: key,
+            KSSAccessKeyId: res.data.kssAccessKeyId,
+            acl: 'public-read',
+            name: fileName
+          }
+
+          let formData = new FormData()
+          for (let key in this.dataObj) {
+            formData.append(key, this.dataObj[key])
+          }
+          formData.append('file', this.blobToFile(data, fileName), fileName)
+          await umiRequest(this.ossUploadUrl, {
+            method: 'POST',
+            data: formData
+          })
+          console.log(this.ossUploadUrl + '/' + key)
+          const uploadUrl = this.ossUploadUrl + '/' + key
+          this.cropperOk(uploadUrl)
+        } catch (err: any) {
+          ElMessage.error(err)
+        } finally {
+          this.submitLoading = false
+          this.cancelHandel()
+        }
+      })
+    },
+    //转成blob
+    blobToFile(Blob: any, fileName: any) {
+      //兼容IE
+      Blob.lastModifiedDate = new Date()
+      Blob.name = fileName
+      return Blob
+    },
+    base64ToFile(urlData: any, fileName: any) {
+      let arr = urlData.split(',')
+      let mime = arr[0].match(/:(.*?);/)[1]
+      let bytes = atob(arr[1]) // 解码base64
+      let n = bytes.length
+      let ia = new Uint8Array(n)
+      while (n--) {
+        ia[n] = bytes.charCodeAt(n)
+      }
+      return new File([ia], fileName, { type: mime })
+    },
+    //移动框的事件
+    realTime(data: any) {
+      this.previews = data
+    },
+    //图片缩放
+    changeScale(num: number) {
+      num = num || 1
+      ;(this as any).$refs.cropperRef.changeScale(num)
+    },
+    //向左旋转
+    rotateLeft() {
+      ;(this as any).$refs.cropperRef.rotateLeft()
+    },
+    //向右旋转
+    rotateRight() {
+      ;(this as any).$refs.cropperRef.rotateRight()
+    }
+  },
+  render() {
+    return (
+      <ElDialog
+        modelValue={this.visible}
+        onUpdate:modelValue={val => (this.visible = val)}
+        appendToBody
+        title={this.options.title}
+        closeOnClickModal={false}
+        width={'800px'}
+        v-slots={{
+          footer: () => (
+            <span class="dialog-footer !text-center block">
+              <ElButton
+                onClick={this.cancelHandel}
+                disabled={this.submitLoading}
+              >
+                取消
+              </ElButton>
+              <ElButton
+                type="primary"
+                onClick={this.okHandel}
+                loading={this.submitLoading}
+              >
+                保 存
+              </ElButton>
+            </span>
+          )
+        }}
+      >
+        <ElRow>
+          <ElCol xs={24} md={12} style={{ width: '350px' }}>
+            <VueCropper
+              ref="cropperRef"
+              img={this.options.img}
+              info={true}
+              autoCrop={this.options.autoCrop}
+              autoCropWidth={this.options.autoCropWidth}
+              full={this.options.full}
+              outputType={this.options.outputType}
+              autoCropHeight={this.options.autoCropHeight}
+              fixedBox={this.options.fixedBox}
+              enlarge={this.options.enlarge}
+              onRealTime={this.realTime}
+              style={{ height: '350px' }}
+            />
+            <div class="flex pt-2">
+              <div
+                onClick={() => {
+                  this.changeScale(1)
+                }}
+                class="mr-2 cursor-pointer"
+                title="放大"
+              >
+                <ElIcon size={30} color="#333">
+                  <CirclePlus />
+                </ElIcon>
+              </div>
+              <div
+                onClick={() => {
+                  this.changeScale(-1)
+                }}
+                class="mr-2 cursor-pointer"
+                title="缩小"
+              >
+                <ElIcon size={30} color="#333">
+                  <Remove />
+                </ElIcon>
+              </div>
+              <div
+                onClick={this.rotateRight}
+                title="向右旋转"
+                class="cursor-pointer"
+              >
+                <img src={iconRate} class="w-[30px] h-[30px]" />
+              </div>
+            </div>
+          </ElCol>
+          <ElCol xs={24} md={12} style={{ height: '350px' }}>
+            <div class={styles.previewImg}>
+              <span>预览图片</span>
+              <div
+                class={
+                  this.options.previewsCircle
+                    ? styles['avatar-upload-preview']
+                    : styles['avatar-upload-preview_range']
+                }
+                style={{
+                  width: this.options.autoCropWidth + 'px',
+                  height: this.options.autoCropHeight + 'px'
+                }}
+              >
+                <ElImage src={this.previews.url} style={this.previews.img} />
+              </div>
+            </div>
+          </ElCol>
+        </ElRow>
+      </ElDialog>
+    )
+  }
+})

+ 85 - 0
src/components/col-cropper-1/index.module.less

@@ -0,0 +1,85 @@
+.uploadSection {
+  width: 156px;
+  height: 106px;
+  background: #f8faf9;
+  border-radius: 4px;
+  border: 1px solid rgba(45, 199, 170, 0.26);
+  font-size: 14px;
+  color: #2dc7aa;
+  line-height: 20px;
+}
+
+.uploadClass {
+  height: 106px;
+  width: 100%;
+
+  :global {
+    .el-loading-spinner {
+      margin-top: -43px;
+      height: 106px;
+    }
+  }
+}
+
+.avatar-upload-preview_range,
+.avatar-upload-preview {
+  width: 180px;
+  height: 180px;
+  box-shadow: 0 0 4px #ccc;
+  overflow: hidden;
+  img {
+    background-color: #f7f7f7;
+    height: 100%;
+  }
+}
+.avatar-upload-preview_range {
+  border-radius: 0;
+}
+.previewImg {
+  padding-left: 50px;
+  padding-top: 10px;
+  & > span {
+    display: block;
+    color: #212121;
+    font-size: 16px;
+    padding-bottom: 15px;
+  }
+}
+.operation {
+  font-size: 24px;
+  display: flex;
+  align-items: center;
+  margin-top: 20px;
+  & > i {
+    margin-left: 12px;
+    cursor: pointer;
+  }
+  .icon-rate {
+    display: inline-block;
+    width: 20px;
+    height: 20px;
+    background: url('./images/icon_rate.png') no-repeat center;
+    background-size: contain;
+  }
+}
+.vue-cropper {
+  border-radius: 5px;
+  overflow: hidden;
+}
+:deep(.el-dialog) {
+  margin-bottom: 10vh;
+  .el-dialog__header {
+    // background: #363d55;
+    background: #fff;
+    padding: 15px 20px 15px;
+    .el-dialog__title {
+      color: #212121;
+    }
+    .el-dialog__headerbtn .el-dialog__close {
+      color: #212121;
+    }
+  }
+  .el-dialog__body {
+    padding-top: 0;
+  }
+}

+ 427 - 0
src/components/col-cropper-1/index.tsx

@@ -0,0 +1,427 @@
+// import { ElImage, ElMessage, ElUpload } from 'element-plus'
+import { defineComponent } from 'vue'
+import styles from './index.module.less'
+import iconUpload from '../col-upload/images/icon_upload.png'
+// import Cropper from './cropper'
+import { VueCropper } from 'vue-cropper'
+import umiRequest from 'umi-request'
+import 'vue-cropper/dist/index.css'
+import {
+  ElButton,
+  ElCol,
+  ElDialog,
+  ElIcon,
+  ElImage,
+  ElMessage,
+  ElUpload,
+  ElRow
+} from 'element-plus'
+import { CirclePlus, Remove } from '@element-plus/icons-vue'
+import iconRate from '../col-upload/images/icon_rate.png'
+import request from '@/helpers/request'
+
+export default defineComponent({
+  name: 'col-cropper',
+  props: {
+    modelValue: {
+      type: String,
+      default: ''
+    },
+    options: {
+      // 裁切需要参数
+      type: Object,
+      default: {
+        autoCrop: true, //是否默认生成截图框
+        enlarge: 1, //  图片放大倍数
+        autoCropWidth: 200, //默认生成截图框宽度
+        autoCropHeight: 200, //默认生成截图框高度
+        fixedBox: true, //是否固定截图框大小 不允许改变
+        previewsCircle: true, //预览图是否是原圆形
+        title: '上传图片'
+      }
+    },
+    // 显示图片原始图片
+    showSize: {
+      type: Boolean,
+      default: false
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    bucket: {
+      type: String,
+      default: 'daya'
+    },
+    size: {
+      type: Number,
+      default: 5 // 默认5M
+    },
+    accept: {
+      type: String,
+      default: 'images/*'
+    },
+    tips: {
+      type: String,
+      default: '请上传图片'
+    },
+    extraTips: {
+      type: String,
+      default: '图片最大不能超过5MB'
+    },
+    cropUploadSuccess: {
+      type: Function,
+      default: (data: string) => {}
+    }
+  },
+  data() {
+    return {
+      isStopRun: false,
+      loading: false,
+      ossUploadUrl: 'https://ks3-cn-beijing.ksyuncs.com/' + this.bucket,
+      dataObj: {
+        policy: '',
+        signature: '',
+        key: '',
+        KSSAccessKeyId: '',
+        acl: 'public-read',
+        name: ''
+      },
+      visible: false,
+      img: null,
+      optionsList: {
+        img: '', //裁剪图片的地址
+        autoCrop: true, //是否默认生成截图框
+        autoCropWidth: 180, //默认生成截图框宽度
+        autoCropHeight: 180, //默认生成截图框高度
+        fixedBox: false, //是否固定截图框大小 不允许改变
+        full: false,
+        enlarge: 1, // 是否按照截图框比例输出 默认为1
+        previewsCircle: true, //预览图是否是原圆形
+        centerBox: true,
+        outputType: 'png',
+        title: '修改头像',
+        name: null // 文件名称
+      },
+      previews: {} as any,
+      url: {
+        upload: '/sys/common/saveToImgByStr'
+      },
+      submitLoading: false
+    }
+  },
+  methods: {
+    onDelete() {
+      // 删除图片
+      this.$emit('update:modelValue', '')
+    },
+    //从本地选择文件
+    async handleChange(info: any) {
+      if (this.isStopRun) {
+        return
+      }
+      this.loading = true
+      const options = this.options
+      this.getBase64(info.file, (imageUrl: any) => {
+        const target = Object.assign({}, options, {
+          img: imageUrl,
+          name: info.file.name // 上传文件名
+        })
+        // ;(this as any).$refs.CropperModal.edit(target)
+        this.edit(target)
+      })
+    },
+    // 上传之前 格式与大小校验
+    beforeUpload(file) {
+      this.isStopRun = false
+      var fileType = file.type
+      if (fileType.indexOf('image') < 0) {
+        ElMessage.warning('请上传图片')
+        this.isStopRun = true
+        return false
+      }
+      // const isJpgOrPng = this.acceptArray.includes(file.type)
+      // if (!isJpgOrPng) {
+      //   ElMessage.error('你上传图片格式不正确!')
+      //   this.isStopRun = true
+      // }
+      console.log(this.size)
+      const size = this.size || 0
+      const isLtSize = file.size < size * 1024 * 1024
+      if (!isLtSize) {
+        ElMessage.error('图片大小不能超过' + this.size + 'MB!')
+        this.isStopRun = true
+      }
+      return isLtSize
+    },
+    error() {
+      this.remove()
+      this.loading = false
+    },
+    remove() {
+      this.onDelete()
+    },
+    //获取服务器返回的地址
+    handleCropperSuccess(data: any) {
+      //将返回的数据回显
+      this.loading = false
+      console.log(data, 'success')
+      this.$emit('update:modelValue', data)
+      // this.cropUploadSuccess(data)
+      // this.$emit('cropUploadSuccess', data)
+      // console.log(this.modelValue, 'modelValue')
+    },
+    // 取消上传
+    handleCropperClose() {
+      this.loading = false
+      this.remove()
+    },
+    getBase64(img, callback) {
+      const reader = new FileReader()
+      reader.addEventListener('load', () => callback(reader.result))
+      reader.readAsDataURL(img)
+    },
+    edit(record: any) {
+      const { options } = this
+      this.visible = true
+      this.optionsList = Object.assign({}, options, record)
+      console.log(this.options)
+    },
+    /**
+     * 取消截图
+     */
+    cancelHandel() {
+      this.visible = false
+      // this.cropperNo()
+      this.loading = false
+      this.remove()
+    },
+    /**
+     * 确认截图
+     */
+    okHandel() {
+      ;(this as any).$refs.cropperRef.getCropBlob(async data => {
+        this.submitLoading = true
+        const options: any = this.options
+        const fileName =
+          (options.name ? options.name.split('.')[0] : +new Date()) + '.png'
+        try {
+          let key = new Date().getTime() + fileName
+          let obj = {
+            filename: fileName,
+            bucketName: this.bucket,
+            postData: {
+              filename: fileName,
+              acl: 'public-read',
+              key: key,
+              unknowValueField: []
+            }
+          }
+          const res = await request.post('/api-website/getUploadSign', {
+            data: obj
+          })
+          this.dataObj = {
+            policy: res.data.policy,
+            signature: res.data.signature,
+            key: key,
+            KSSAccessKeyId: res.data.kssAccessKeyId,
+            acl: 'public-read',
+            name: fileName
+          }
+
+          let formData = new FormData()
+          for (let key in this.dataObj) {
+            formData.append(key, this.dataObj[key])
+          }
+          formData.append('file', this.blobToFile(data, fileName), fileName)
+          await umiRequest(this.ossUploadUrl, {
+            method: 'POST',
+            data: formData
+          })
+          console.log(this.ossUploadUrl + '/' + key)
+          const uploadUrl = this.ossUploadUrl + '/' + key
+          // this.cropperOk(uploadUrl)
+          this.$emit('update:modelValue', uploadUrl)
+        } catch (err: any) {
+          ElMessage.error(err)
+        } finally {
+          this.submitLoading = false
+          this.cancelHandel()
+        }
+      })
+    },
+    //转成blob
+    blobToFile(Blob: any, fileName: any) {
+      //兼容IE
+      Blob.lastModifiedDate = new Date()
+      Blob.name = fileName
+      return Blob
+    },
+    base64ToFile(urlData: any, fileName: any) {
+      let arr = urlData.split(',')
+      let mime = arr[0].match(/:(.*?);/)[1]
+      let bytes = atob(arr[1]) // 解码base64
+      let n = bytes.length
+      let ia = new Uint8Array(n)
+      while (n--) {
+        ia[n] = bytes.charCodeAt(n)
+      }
+      return new File([ia], fileName, { type: mime })
+    },
+    //移动框的事件
+    realTime(data: any) {
+      this.previews = data
+    },
+    //图片缩放
+    changeScale(num: number) {
+      num = num || 1
+      ;(this as any).$refs.cropperRef.changeScale(num)
+    },
+    //向左旋转
+    rotateLeft() {
+      ;(this as any).$refs.cropperRef.rotateLeft()
+    },
+    //向右旋转
+    rotateRight() {
+      ;(this as any).$refs.cropperRef.rotateRight()
+    }
+  },
+  render() {
+    return (
+      <div class={[styles.colUpload, 'w-full']}>
+        <ElUpload
+          disabled={this.disabled}
+          showFileList={false}
+          accept={this.accept}
+          beforeUpload={this.beforeUpload}
+          // @ts-ignore
+          httpRequest={this.handleChange}
+          // limit={1}
+          ref="uploadRef"
+        >
+          <div
+            ref="uploadDom"
+            class={[styles.uploadClass, 'w-full']}
+            style={{ height: '106px' }}
+          >
+            {this.modelValue ? (
+              <ElImage
+                src={this.modelValue}
+                fit="cover"
+                class={styles.uploadSection}
+              />
+            ) : (
+              <div
+                class={[
+                  styles.uploadSection,
+                  'flex items-center flex-col justify-center'
+                ]}
+              >
+                <img src={iconUpload} class="w-8 h-7 mb-3" />
+                <p>{this.tips}</p>
+              </div>
+            )}
+          </div>
+        </ElUpload>
+
+        <p class="text-3 text-[#999999] leading-6 pt-1">{this.extraTips}</p>
+
+        <ElDialog
+          modelValue={this.visible}
+          onUpdate:modelValue={val => (this.visible = val)}
+          appendToBody
+          title={this.options.title}
+          closeOnClickModal={false}
+          width={'800px'}
+          v-slots={{
+            footer: () => (
+              <span class="dialog-footer !text-center block">
+                <ElButton
+                  onClick={this.cancelHandel}
+                  disabled={this.submitLoading}
+                >
+                  取消
+                </ElButton>
+                <ElButton
+                  type="primary"
+                  onClick={this.okHandel}
+                  loading={this.submitLoading}
+                >
+                  保 存
+                </ElButton>
+              </span>
+            )
+          }}
+        >
+          <ElRow>
+            <ElCol xs={24} md={12} style={{ width: '350px' }}>
+              <VueCropper
+                ref="cropperRef"
+                img={this.optionsList.img}
+                info={true}
+                autoCrop={this.optionsList.autoCrop}
+                autoCropWidth={this.optionsList.autoCropWidth}
+                full={this.optionsList.full}
+                outputType={this.optionsList.outputType}
+                autoCropHeight={this.optionsList.autoCropHeight}
+                fixedBox={this.optionsList.fixedBox}
+                enlarge={this.optionsList.enlarge}
+                onRealTime={this.realTime}
+                style={{ height: '350px' }}
+              />
+              <div class="flex pt-2">
+                <div
+                  onClick={() => {
+                    this.changeScale(1)
+                  }}
+                  class="mr-2 cursor-pointer"
+                  title="放大"
+                >
+                  <ElIcon size={30} color="#333">
+                    <CirclePlus />
+                  </ElIcon>
+                </div>
+                <div
+                  onClick={() => {
+                    this.changeScale(-1)
+                  }}
+                  class="mr-2 cursor-pointer"
+                  title="缩小"
+                >
+                  <ElIcon size={30} color="#333">
+                    <Remove />
+                  </ElIcon>
+                </div>
+                <div
+                  onClick={this.rotateRight}
+                  title="向右旋转"
+                  class="cursor-pointer"
+                >
+                  <img src={iconRate} class="w-[30px] h-[30px]" />
+                </div>
+              </div>
+            </ElCol>
+            <ElCol xs={24} md={12} style={{ height: '350px' }}>
+              <div class={styles.previewImg}>
+                <span>预览图片</span>
+                <div
+                  class={
+                    this.optionsList.previewsCircle
+                      ? styles['avatar-upload-preview']
+                      : styles['avatar-upload-preview_range']
+                  }
+                  style={{
+                    width: this.optionsList.autoCropWidth + 'px',
+                    height: this.optionsList.autoCropHeight + 'px'
+                  }}
+                >
+                  <ElImage src={this.previews.url} style={this.previews.img} />
+                </div>
+              </div>
+            </ElCol>
+          </ElRow>
+        </ElDialog>
+      </div>
+    )
+  }
+})

+ 1 - 1
src/components/col-cropper/cropper.tsx

@@ -21,7 +21,7 @@ export default defineComponent({
   props: {
     cropperNo: {
       type: Function,
-      default: () => {}
+      default: (data: any) => {}
     },
     cropperOk: {
       type: Function,

+ 1 - 1
src/components/col-cropper/index.tsx

@@ -120,7 +120,7 @@ export default defineComponent({
       this.loading = false
       console.log(data, 'success')
       this.$emit('update:modelValue', data)
-      // this.cropUploadSuccess(data)
+      this.cropUploadSuccess(data)
       // this.$emit('cropUploadSuccess', data)
       // console.log(this.modelValue, 'modelValue')
     },

+ 13 - 1
src/components/col-upload-video/index.module.less

@@ -27,7 +27,15 @@
 }
 
 .fileUpload {
+  .uploadClass {
+    position: relative;
+    display: flex;
+    width: 96px;
+  }
   :global {
+    .el-upload {
+      justify-content: flex-start;
+    }
     .el-upload--text {
       @apply w-full;
     }
@@ -35,7 +43,7 @@
     .el-loading-spinner {
       display: flex;
       align-items: center;
-      height: 42px !important;
+      height: 40px !important;
       justify-content: center;
       margin-top: -20px !important;
       svg {
@@ -47,6 +55,10 @@
   }
 }
 
+.disabled .uploadClass {
+  cursor: not-allowed !important;
+}
+
 .uploadClass {
   height: 106px;
   width: 100%;

+ 65 - 21
src/components/col-upload-video/index.tsx

@@ -32,6 +32,10 @@ export default defineComponent({
       type: Boolean,
       default: false
     },
+    limit: {
+      type: Number,
+      default: 1
+    },
     size: {
       type: Number,
       default: 50 // 默认5M
@@ -48,6 +52,10 @@ export default defineComponent({
     extraTips: {
       type: String,
       default: '视频最大不能超过50MB'
+    },
+    multipleModel: {
+      type: Function,
+      default: (data: any) => {}
     }
   },
   data() {
@@ -62,15 +70,32 @@ export default defineComponent({
         name: ''
       },
       fileList: [] as any,
+      tempUrls: {} as any,
+      responseList: [] as any, // 请求成功之后的数据
+      btnLoading: false,
       loading: null as any
     }
   },
   methods: {
-    handleSuccess() {
+    handleSuccess(response: any, uploadFile: any, uploadFiles: any) {
       this.loading?.close()
-      let url = this.ossUploadUrl + '/' + this.dataObj.key
 
-      this.$emit('update:modelValue', url)
+      // 多文件上传,每个文件上传成功后,将文件的url添加到fileList
+      console.log(this.fileList, 'fileList')
+      console.log(response, uploadFile, uploadFiles, 'response')
+      if (this.multiple) {
+        if (uploadFile.status === 'success') {
+          this.responseList.push(this.tempUrls[uploadFile.uid])
+        }
+        // 说明已经上传完成
+        if (uploadFiles.length === this.responseList.length) {
+          this.btnLoading = false
+          this.multipleModel(this.responseList)
+        }
+      } else {
+        let url = this.ossUploadUrl + '/' + this.dataObj.key
+        this.$emit('update:modelValue', url)
+      }
     },
     handleRemove() {
       console.log('remove')
@@ -78,10 +103,11 @@ export default defineComponent({
     handleChange() {
       console.log('handleChange')
     },
-    handleProgress() {
-      console.log('handleProgress')
+    handleProgress(e) {
+      console.log('handleProgress', e)
     },
     handleError() {
+      this.btnLoading = false
       this.loading?.close()
     },
     async beforeUpload(file: any) {
@@ -112,14 +138,17 @@ export default defineComponent({
           return false
         }
       }
-      this.loading = ElLoading.service({
-        target: this.$refs.uploadDom as HTMLElement,
-        lock: true,
-        fullscreen: false,
-        text: '上传中...',
-        background: 'rgba(0, 0, 0, 0.7)'
-      })
-      console.log(this.loading)
+      if (this.multiple) {
+        this.btnLoading = true
+      } else {
+        this.loading = ElLoading.service({
+          target: this.$refs.uploadDom as HTMLElement,
+          lock: true,
+          fullscreen: false,
+          text: '上传中...',
+          background: 'rgba(0, 0, 0, 0.7)'
+        })
+      }
       try {
         let fileName = file.name.replaceAll(' ', '_')
         let key = new Date().getTime() + fileName
@@ -144,14 +173,21 @@ export default defineComponent({
           acl: 'public-read',
           name: fileName
         }
+        this.tempUrls[file.uid] = this.ossUploadUrl + '/' + this.dataObj.key
       } catch (e) {
-        this.loading.close()
+        this.btnLoading = false
+        this.loading?.close()
       }
     },
     fileName(name = '') {
       return name.split('/').pop()
     },
-    handleExceed() {}
+    handleExceed(e: any) {
+      if (e.length > this.limit) {
+        ElMessage.error(`一次性最多只能上传${this.limit}个文件`)
+        return false
+      }
+    }
   },
   render() {
     return (
@@ -171,21 +207,29 @@ export default defineComponent({
           beforeUpload={this.beforeUpload}
           onExceed={this.handleExceed}
           ref="uploadRef"
+          multiple={this.multiple}
+          limit={this.limit}
+          class={[
+            this.multiple && styles.fileUpload,
+            this.disabled && styles.disabled
+          ]}
         >
           <div
             ref="uploadDom"
             class={[styles.uploadClass, 'w-full']}
-            style={{ height: '106px' }}
+            style={{ height: this.multiple ? '40px' : '106px' }}
           >
             {this.modelValue ? (
-              <ElImage
-                src={this.modelValue}
-                fit="cover"
+              <video
+                ref="videoUpload"
+                crossorigin="anonymous"
                 class={styles.uploadSection}
+                src={this.modelValue}
+                // poster={iconUploadPoster}
               />
             ) : this.multiple ? (
-              <ElButton size="large" type="primary">
-                点击上传
+              <ElButton size="large" type="primary" loading={this.btnLoading}>
+                {this.btnLoading ? '上传中...' : '点击上传'}
               </ElButton>
             ) : (
               <div

+ 5 - 0
src/components/col-upload/index.tsx

@@ -39,6 +39,10 @@ export default defineComponent({
     extraTips: {
       type: String,
       default: '图片最大不能超过5MB'
+    },
+    onChange: {
+      type: Function,
+      default: () => {}
     }
   },
   data() {
@@ -62,6 +66,7 @@ export default defineComponent({
       let url = this.ossUploadUrl + '/' + this.dataObj.key
       console.log(url)
       this.$emit('update:modelValue', url)
+      this.onChange(url)
     },
     handleRemove() {
       console.log('remove')

+ 0 - 7
src/views/user-info/components/user-menu/index.tsx

@@ -39,13 +39,6 @@ export default defineComponent({
       return list
     }
   },
-  mounted() {
-    // console.log(this.$router.getRoutes(), this.$router.currentRoute.getRoutes())
-    // console.log(this.$router)
-    // const currentRoute = this.$router.currentRoute
-    // console.log(currentRoute, currentRoute.value.name)
-    // console.log(this.$router.hasRoute(currentRoute.value.name))
-  },
   render() {
     return (
       <div class="bg-white rounded-[6px] text-center py-6 px-[18px] flex items-center flex-col">

+ 1 - 1
src/views/user-info/live-class/list.tsx

@@ -55,7 +55,7 @@ export default defineComponent({
       } else {
         setTimeout(() => {
           this.loading = false
-        }, 500)
+        }, 200)
       }
     }
   },

+ 1 - 1
src/views/user-info/music-class/list.tsx

@@ -66,7 +66,7 @@ export default defineComponent({
       } else {
         setTimeout(() => {
           this.loading = false
-        }, 500)
+        }, 200)
       }
     }
   },

+ 9 - 0
src/views/user-info/practice-setting/index.tsx

@@ -234,6 +234,15 @@ export default defineComponent({
               postMessage({ api: 'back', content: {} })
             }, 500)
           } catch {}
+        } else {
+          this.$nextTick(() => {
+            let isError = document.getElementsByClassName('is-error')
+            isError[0].scrollIntoView({
+              block: 'center',
+              behavior: 'smooth'
+            })
+          })
+          return false
         }
       })
     }

+ 7 - 5
src/views/user-info/video-class/index.tsx

@@ -7,8 +7,10 @@ import List from './list'
 export default defineComponent({
   name: 'video-class',
   data() {
+    const videoActiveName = sessionStorage.getItem('videoActiveName')
+    sessionStorage.removeItem('videoActiveName')
     return {
-      activeName: 'DOING'
+      activeName: videoActiveName || 'PASS'
     }
   },
   render() {
@@ -27,12 +29,12 @@ export default defineComponent({
           新建视频课
         </ElButton>
         <ElTabs v-model={this.activeName}>
-          <ElTabPane label="已上架" name="DOING">
-            {this.activeName === 'DOING' && <List auditStatus="DOING" />}
-          </ElTabPane>
-          <ElTabPane label="审核中" name="PASS">
+          <ElTabPane label="已上架" name="PASS">
             {this.activeName === 'PASS' && <List auditStatus="PASS" />}
           </ElTabPane>
+          <ElTabPane label="审核中" name="DOING">
+            {this.activeName === 'DOING' && <List auditStatus="DOING" />}
+          </ElTabPane>
           <ElTabPane label="审核失败" name="UNPASS">
             {this.activeName === 'UNPASS' && <List auditStatus="UNPASS" />}
           </ElTabPane>

+ 16 - 2
src/views/user-info/video-class/list.tsx

@@ -57,7 +57,21 @@ export default defineComponent({
       } else {
         setTimeout(() => {
           this.loading = false
-        }, 500)
+        }, 200)
+      }
+    },
+    onDetail(item: any) {
+      if (this.auditStatus === 'UNPASS') {
+        // this.$message.error('该课程正在审核中,请等待审核结果')
+        this.$router.push({
+          path: '/userInfo/videoOperation',
+          query: {
+            type: 'edit',
+            groupId: item.id
+          }
+        })
+        return
+      } else {
       }
     }
   },
@@ -115,7 +129,7 @@ export default defineComponent({
             }}
           >
             {this.list.map((item: any) => (
-              <div class="w-1/3 pt-8">
+              <div class="w-1/3 pt-8" onClick={() => this.onDetail(item)}>
                 <Item
                   item={{
                     backgroundPic: item.lessonCoverUrl,

+ 14 - 0
src/views/user-info/video-operation/course-content/index.module.less

@@ -0,0 +1,14 @@
+.courseContent {
+  :global {
+    .el-dialog {
+      --el-dialog-width: 375px !important;
+    }
+
+    .el-dialog__body {
+      padding: 0;
+    }
+    .el-dialog__footer {
+      background-color: #f6f8f9;
+    }
+  }
+}

+ 146 - 13
src/views/user-info/video-operation/course-content/index.tsx

@@ -1,42 +1,144 @@
 import ColUpload from '@/components/col-upload'
 import ColUploadVideo from '@/components/col-upload-video'
+import request from '@/helpers/request'
+import { scrollAnimation } from '@/util/scroll'
 import {
   ElButton,
   ElCol,
+  ElDialog,
   ElForm,
   ElFormItem,
   ElInput,
+  ElMessage,
   ElRow
 } from 'element-plus'
 import { defineComponent } from 'vue'
+import CoursePreview from '../course-preview'
 import { createState } from '../createState'
+import styles from './index.module.less'
 
 export default defineComponent({
   name: 'course-content',
+  data() {
+    return {
+      show: false,
+      form: {
+        lessonList: [
+          {
+            key: 1,
+            videoTitle: '',
+            videoContent: '',
+            videoUrl: '',
+            coverUrl: ''
+          }
+        ] as any
+      }
+    }
+  },
+  methods: {
+    handleUpload(urls: any) {
+      console.log(urls)
+      // 上传视频连接
+      const urlList = urls || []
+      urlList.forEach((item: any) => {
+        createState.lessonList.push({
+          videoTitle: '',
+          videoContent: '',
+          videoUrl: item,
+          coverUrl: ''
+        })
+      })
+    },
+    handleSubmit() {
+      ;(this as any).$refs.form.validate(async (valid: any) => {
+        if (valid) {
+          if (createState.lessonList.length <= 0) {
+            ElMessage.error('请上传课程视频')
+            return
+          }
+          // 提交数据
+          console.log('提交数据')
+          this.show = true
+        } else {
+          this.$nextTick(() => {
+            let isError = document.getElementsByClassName('is-error')
+            isError[0].scrollIntoView({
+              block: 'center',
+              behavior: 'smooth'
+            })
+          })
+          return false
+        }
+      })
+    },
+    async createSubmit() {
+      try {
+        const videoDetail = createState.lessonGroup
+        let params = {
+          lessonList: createState.lessonList,
+          lessonGroup: {
+            ...videoDetail,
+            lessonCoverUrl:
+              videoDetail.lessonCoverTemplateUrl || videoDetail.lessonCoverUrl
+          }
+        }
+        if (createState.groupId) {
+          await request.post('/api-website/videoLessonGroup/update', {
+            data: params
+          })
+          ElMessage.success('修改成功')
+        } else {
+          await request.post('/api-website/videoLessonGroup/add', {
+            data: params
+          })
+          ElMessage.success('创建成功')
+        }
+        sessionStorage.setItem('videoActiveName', 'DOING')
+        this.$router.back()
+      } catch {}
+    }
+  },
   render() {
     return (
-      <>
+      <div class={styles.courseContent}>
         <ElForm
-          class="px-[200px] pb-10 pt-7"
+          class="px-[140px] pb-10 pt-7 min-h-[280px]"
           size="large"
           labelWidth={'90px'}
           labelPosition="left"
-          model={createState.lessonList}
+          ref="form"
+          model={createState}
         >
-          <ElFormItem label="课程视频">
-            <ColUploadVideo multiple />
+          <ElFormItem label="课程视频" required>
+            <ColUploadVideo
+              multiple
+              limit={3}
+              bucket="video-course"
+              multipleModel={(urls: any) => {
+                this.handleUpload(urls)
+              }}
+            />
             {/* <ElInput placeholder="请输入课程名称" /> */}
           </ElFormItem>
           {createState.lessonList.map((item: any, index: number) => (
-            <div class="p-4 rounded-xl border border-dashed border-gray-300">
+            <div
+              class="p-4 pt-5 rounded-xl mb-3 border border-dashed border-gray-300"
+              key={item.key}
+            >
               <ElRow>
-                <ElCol span={12}>
-                  <ElFormItem label="第一课"></ElFormItem>
+                <ElCol span={10}>
+                  <ElFormItem label="第一课">
+                    <ColUploadVideo
+                      v-model:modelValue={item.videoUrl}
+                      disabled
+                    />
+                  </ElFormItem>
                 </ElCol>
-                <ElCol span={12}>
+                <ElCol span={10}>
                   <ElFormItem
                     label="视频封面"
                     prop={`lessonList.${index}.coverUrl`}
+                    rules={[{ required: true, message: '请上传视频封面' }]}
                   >
                     <ColUpload v-model:modelValue={item.coverUrl} />
                   </ElFormItem>
@@ -86,6 +188,9 @@ export default defineComponent({
             class="!w-40 !h-[38px]"
             onClick={() => {
               createState.active = 0
+              const currentY =
+                document.documentElement.scrollTop || document.body.scrollTop
+              scrollAnimation(currentY, 0)
             }}
           >
             上一步
@@ -93,14 +198,42 @@ export default defineComponent({
           <ElButton
             type="primary"
             class="!w-40 !h-[38px]"
-            onClick={() => {
-              createState.active = 1
-            }}
+            onClick={this.handleSubmit}
           >
             下一步
           </ElButton>
         </div>
-      </>
+        {/*  @ts-ignore */}
+        <ElDialog
+          v-model={this.show}
+          title="预览"
+          v-slots={{
+            footer: () => (
+              <span class="dialog-footer !text-center block ">
+                <ElButton
+                  size="large"
+                  round
+                  onClick={() => {
+                    this.show = false
+                  }}
+                >
+                  返回编辑
+                </ElButton>
+                <ElButton
+                  size="large"
+                  round
+                  type="primary"
+                  onClick={this.createSubmit}
+                >
+                  创建完成
+                </ElButton>
+              </span>
+            )
+          }}
+        >
+          <CoursePreview />
+        </ElDialog>
+      </div>
     )
   }
 })

+ 7 - 0
src/views/user-info/video-operation/course-info/index.module.less

@@ -0,0 +1,7 @@
+.tabs {
+  :global {
+    .el-tabs__nav-wrap::after {
+      background-color: transparent;
+    }
+  }
+}

+ 46 - 22
src/views/user-info/video-operation/course-info/index.tsx

@@ -1,5 +1,5 @@
 import ColCropper from '@/components/col-cropper'
-import ColUpload from '@/components/col-upload'
+import styles from './index.module.less'
 import request from '@/helpers/request'
 import { verifyNumberIntegerAndFloat } from '@/helpers/toolsValidate'
 import {
@@ -19,6 +19,7 @@ import {
 } from 'element-plus'
 import { defineComponent } from 'vue'
 import { createState } from '../createState'
+import { scrollAnimation } from '@/util/scroll'
 
 export default defineComponent({
   name: 'course-info',
@@ -194,6 +195,7 @@ export default defineComponent({
           <ElFormItem label="课程封面" class="!mb-0">
             <ElTabs
               v-model={createState.tabIndex}
+              class={styles.tabs}
               onTab-change={(name: any) => {
                 this.tabChange(name)
               }}
@@ -205,7 +207,7 @@ export default defineComponent({
 
           {createState.tabIndex === 1 && (
             <ElFormItem
-              prop="lessonCoverUrl"
+              prop="lessonCoverTemplateUrl"
               rules={[
                 {
                   required: true,
@@ -213,15 +215,22 @@ export default defineComponent({
                 }
               ]}
             >
-              <ElRadioGroup v-model={createState.lessonGroup.lessonCoverUrl}>
+              <ElRadioGroup
+                v-model={createState.lessonGroup.lessonCoverTemplateUrl}
+              >
                 <ElRow>
                   {createState.templateList.map((item: any) => (
                     <ElCol span={10} class="mb-3 cursor-pointer">
-                      <div class="w-40 relative rounded-xl overflow-hidden border">
+                      <div
+                        class="w-40 relative rounded-xl overflow-hidden border"
+                        onClick={() => {
+                          this.selectImg(item)
+                        }}
+                      >
                         <ElImage src={item} class="align-middle" />
                         <ElRadio
                           label={item}
-                          class="!absolute bottom-2 right-0 !h-auto"
+                          class="!absolute bottom-2 right-0 !h-auto z-10"
                         >
                           {''}
                         </ElRadio>
@@ -235,7 +244,7 @@ export default defineComponent({
 
           {createState.tabIndex === 2 && (
             <ElFormItem
-              prop="lessonCoverTemplateUrl"
+              prop="lessonCoverUrl"
               rules={[
                 {
                   required: true,
@@ -244,25 +253,27 @@ export default defineComponent({
                 }
               ]}
             >
-              <ColUpload
-                v-model:modelValue={
-                  createState.lessonGroup.lessonCoverTemplateUrl
-                }
+              {/* <ColUpload
+                v-model:modelValue={createState.lessonGroup.lessonCoverUrl}
+                onChange={(value: any) => {
+                  createState.lessonGroup.lessonCoverTemplateUrl = ''
+                }}
+              /> */}
+              <ColCropper
+                modelValue={createState.lessonGroup.lessonCoverUrl}
+                bucket="video-course"
+                cropUploadSuccess={(data: any) => {
+                  createState.lessonGroup.lessonCoverUrl = data
+                  createState.lessonGroup.lessonCoverTemplateUrl = ''
+                }}
+                options={{
+                  fixedNumber: [3, 2],
+                  autoCropWidth: 300,
+                  autoCropHeight: 200
+                }}
               />
             </ElFormItem>
           )}
-
-          {/* <ColCropper
-              v-model:modelValue={
-                createState.lessonGroup.lessonCoverTemplateUrl
-              }
-              bucket="video-course"
-              options={{
-                fixedNumber: [3, 2],
-                autoCropWidth: 300,
-                autoCropHeight: 200
-              }}
-            /> */}
         </ElForm>
         <div class="border-t border-t-[#E5E5E5] text-center pt-6 pb-7">
           <ElButton
@@ -272,6 +283,19 @@ export default defineComponent({
               ;(this as any).$refs.form.validate(async (valid: boolean) => {
                 if (valid) {
                   createState.active = 1
+                  const currentY =
+                    document.documentElement.scrollTop ||
+                    document.body.scrollTop
+                  scrollAnimation(currentY, 0)
+                } else {
+                  this.$nextTick(() => {
+                    let isError = document.getElementsByClassName('is-error')
+                    isError[0].scrollIntoView({
+                      block: 'center',
+                      behavior: 'smooth'
+                    })
+                  })
+                  return false
                 }
               })
             }}

+ 134 - 0
src/views/user-info/video-operation/course-preview/index.module.less

@@ -0,0 +1,134 @@
+.course-preview {
+  // padding: 0 14px;
+  background-color: #f6f8f9;
+  max-height: 500px;
+  overflow-y: auto;
+  overflow-x: hidden;
+}
+
+.createSubmit {
+  // margin-bottom: 12px;
+
+  .introduction {
+    color: #7a7a7a;
+    line-height: 23px;
+    padding-bottom: 8px;
+  }
+}
+
+.userDetail {
+  .banner {
+    width: 100%;
+    height: 210px;
+    overflow: hidden;
+    vertical-align: middle;
+  }
+
+  .userInfo {
+    overflow: hidden;
+    border-top: 1px solid #ebebeb;
+    display: flex;
+    justify-content: space-between;
+
+    .avatar {
+      width: 24px;
+      height: 24px;
+      border-radius: 50%;
+      overflow: hidden;
+    }
+
+    .name {
+      padding-left: 8px;
+      font-size: 16px;
+      font-weight: 500;
+      color: #333333;
+      line-height: 22px;
+      display: flex;
+      align-items: center;
+    }
+
+    .buyNum {
+      color: #ff802c;
+      font-size: 14px;
+      display: flex;
+      align-items: center;
+      &::before {
+        content: ' ';
+        display: inline-block;
+        width: 1px;
+        height: 12px;
+        margin: 0 8px;
+        background: #d3d3d3;
+      }
+    }
+
+    .buyNumInfo {
+      font-size: 12px;
+      color: #6a6a6a;
+      display: flex;
+      align-items: center;
+      .iconBuy {
+        margin-right: 5px;
+      }
+    }
+
+    .info {
+      font-size: 16px;
+      font-weight: 400;
+      color: var(--van-primary);
+      line-height: 17px;
+    }
+
+    .userTitle {
+      font-size: 18px;
+      color: #1a1a1a;
+      font-weight: 500;
+      display: block !important;
+    }
+  }
+}
+
+.videoImg {
+  width: 100px;
+  height: 70px;
+  position: relative;
+  border-radius: 4px;
+  overflow: hidden;
+
+  .videoStop {
+    position: absolute;
+    top: 50%;
+    right: 0;
+    left: 50%;
+    bottom: 0;
+    margin-top: -13px;
+    margin-left: -13px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+}
+
+.videoTitle {
+  padding-left: 8px;
+  font-size: 13px;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  justify-content: space-around;
+  .videoTitleText {
+    font-size: 15px;
+    color: #000;
+    max-width: 210px;
+  }
+  .videoTitleContent {
+    color: #7a7a7a;
+    line-height: 18px;
+
+    display: -webkit-box;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    -webkit-line-clamp: 2;
+    -webkit-box-orient: vertical;
+  }
+}

+ 133 - 0
src/views/user-info/video-operation/course-preview/index.tsx

@@ -0,0 +1,133 @@
+import { state } from '@/state'
+import { ElIcon, ElImage } from 'element-plus'
+import { defineComponent } from 'vue'
+import { createState } from '../createState'
+import styles from './index.module.less'
+import defaultIcon from '@common/images/icon_teacher.png'
+import iconIn from '../images/icon_course_introduction.png'
+import iconList from '../images/icon_course_list.png'
+import videoStop from '../images/icon_video_stop.png'
+
+export default defineComponent({
+  name: 'course-preview',
+  computed: {
+    userInfo() {
+      const videoDetail = createState.lessonGroup
+      const users = state.user.data || {}
+      return {
+        username: users.username || `游客${users.id || ''}`,
+        headUrl: users.headUrl,
+        lessonName: videoDetail.lessonName,
+        buyNum: 0,
+        lessonDesc: videoDetail.lessonDesc,
+        lessonPrice: videoDetail.lessonPrice,
+        lessonCoverUrl:
+          videoDetail.lessonCoverTemplateUrl || videoDetail.lessonCoverUrl,
+        lessonNum: createState.lessonList.length
+      }
+    },
+    lessonList() {
+      return createState.lessonList || []
+    }
+  },
+  render() {
+    return (
+      <div class={[styles['course-preview'], 'pb-3']}>
+        <div class={styles.userDetail}>
+          <ElImage
+            class={[styles.banner]}
+            src={this.userInfo.lessonCoverUrl}
+            fit="cover"
+          />
+
+          <div class="bg-white">
+            <div class="p-[14px] text-lg text-[#1a1a1a] font-medium leading-none">
+              标题
+            </div>
+            <div class={[styles.userInfo, 'mx-[14px] py-[14px]']}>
+              <div class="flex">
+                <ElImage
+                  class={styles.avatar}
+                  src={this.userInfo.headUrl || defaultIcon}
+                  fit=""
+                />
+                <div class={styles.name}>
+                  {this.userInfo.username || `游客${this.userInfo.id || ''}`}
+
+                  <div class={styles.buyNum}>
+                    {this.userInfo.buyNum}人已购买
+                  </div>
+                </div>
+              </div>
+              <div class={styles.info}>
+                ¥{this.userInfo.lessonPrice}/{this.userInfo.lessonNum}
+                课时
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div
+          class={[
+            styles['section-detail'],
+            'mt-[10px] mx-[14px] rounded-lg bg-white'
+          ]}
+        >
+          <div class="flex items-center py-3 px-[10px] text-[#333333] text-base">
+            <ElIcon size={18} class="mr-2">
+              <img src={iconIn} />
+            </ElIcon>
+            课程介绍
+          </div>
+
+          <div class="mx-[10px] pt-[10px] pb-4 text-sm text-[#7A7A7A] border-t border-t-[#EBEBEB]">
+            {this.userInfo.lessonDesc}
+          </div>
+        </div>
+
+        <div
+          class={[
+            styles['section-detail'],
+            'mt-[10px] mx-[14px] rounded-lg bg-white'
+          ]}
+        >
+          <div class="flex items-center py-3 px-[10px] text-[#333333] text-base">
+            <ElIcon size={18} class="mr-2">
+              <img src={iconList} />
+            </ElIcon>
+            课程列表
+          </div>
+
+          <div class="mx-[10px] pt-[10px] pb-4 text-sm text-[#7A7A7A] border-t border-t-[#EBEBEB] flex flex-col">
+            {createState.lessonList.map((item: any) => (
+              <div class="flex mb-3">
+                <div class={styles.videoImg}>
+                  <ElImage
+                    class="align-middle h-[70px] w-[100px]"
+                    src={item.coverUrl}
+                    fit="cover"
+                  />
+                  <ElIcon class={styles.videoStop} size={26}>
+                    <img src={videoStop} />
+                  </ElIcon>
+                </div>
+
+                <div class={[styles.videoTitle, '!h-[70px]']}>
+                  <p
+                    class={[
+                      styles.videoTitleText,
+                      'whitespace-nowrap overflow-hidden text-ellipsis'
+                    ]}
+                  >
+                    {item.videoTitle}
+                  </p>
+                  <p class={[styles.videoTitleContent]}>{item.videoContent}</p>
+                </div>
+              </div>
+            ))}
+          </div>
+        </div>
+      </div>
+    )
+  }
+})

+ 41 - 32
src/views/user-info/video-operation/createState.tsx

@@ -1,34 +1,43 @@
 import { reactive } from 'vue'
 
-export const createState = reactive({
-  groupId: 0,
-  active: 1,
-  tabIndex: 1,
-  loadingStatus: false,
-  rate: 0, // 手续费
-  subjectList: [], // 声部列表
-  templateList: [
-    'https://daya.ks3-cn-beijing.ksyun.com/202204/T3unJdc.png',
-    'https://daya.ks3-cn-beijing.ksyun.com/202204/T3unJdl.png',
-    'https://daya.ks3-cn-beijing.ksyun.com/202204/T3unJdK.png',
-    'https://daya.ks3-cn-beijing.ksyun.com/202204/T3unJeA.png'
-  ], // 模板列表
-  lessonGroup: {
-    id: null,
-    lessonName: '',
-    lessonSubject: null as any,
-    lessonDesc: '',
-    lessonPrice: null as any,
-    lessonCoverUrl: '',
-    lessonCoverTemplateUrl: ''
-  },
-  lessonList: [
-    {
-      videoTitle: '',
-      videoContent: '',
-      videoUrl: '',
-      coverUrl: '',
-      posterUrl: '' // 视频封面图
-    }
-  ]
-})
+const original = () => {
+  return {
+    groupId: 0,
+    active: 0,
+    tabIndex: 1,
+    loadingStatus: false,
+    rate: 0, // 手续费
+    subjectList: [], // 声部列表
+    templateList: [
+      'https://daya.ks3-cn-beijing.ksyun.com/202204/T3unJdc.png',
+      'https://daya.ks3-cn-beijing.ksyun.com/202204/T3unJdl.png',
+      'https://daya.ks3-cn-beijing.ksyun.com/202204/T3unJdK.png',
+      'https://daya.ks3-cn-beijing.ksyun.com/202204/T3unJeA.png'
+    ], // 模板列表
+    lessonGroup: {
+      id: null,
+      lessonName: '',
+      lessonSubject: null as any,
+      lessonDesc: '',
+      lessonPrice: null as any,
+      lessonCoverUrl: '',
+      lessonCoverTemplateUrl: ''
+    },
+    lessonList: [
+      // {
+      //   videoTitle: '',
+      //   videoContent: '',
+      //   videoUrl: '',
+      //   coverUrl: '',
+      //   posterUrl: '' // 视频封面图
+      // }
+    ] as any
+  }
+}
+
+export const createState = reactive(original())
+
+// 重置对象
+export const resestState = () => {
+  Object.assign(createState, original())
+}

二進制
src/views/user-info/video-operation/images/icon_course_introduction.png


二進制
src/views/user-info/video-operation/images/icon_course_list.png


二進制
src/views/user-info/video-operation/images/icon_video_stop.png


+ 3 - 1
src/views/user-info/video-operation/index.tsx

@@ -3,7 +3,7 @@ import request from '@/helpers/request'
 import { defineComponent } from 'vue'
 import CourseContent from './course-content'
 import CourseInfo from './course-info'
-import { createState } from './createState'
+import { createState, resestState } from './createState'
 
 export default defineComponent({
   name: 'video-operation',
@@ -14,6 +14,8 @@ export default defineComponent({
     }
   },
   async created() {
+    // 重置数据
+    resestState()
     const query = this.$route.query
     createState.groupId = Number(query.groupId) || 0
     // 判断是否是编辑