<template>
  <!--upload 上传组件-->
  <div class="def-upload">
    <el-upload
      v-model:file-list="fileListVal"
      :action="action"
      :multiple="multiple"
      :limit="limit"
      :disabled="disabledVal"
      :show-file-list="showFileList"
      :list-type="listType"
      :drag="drag"
      v-bind="$attrs"
      :on-preview="onPreview"
      :on-remove="onRemove"
      :on-success="onSuccess"
      :on-error="onError"
      :on-progress="onProgress"
      :on-change="onChange"
      :on-exceed="onExceed"
      :before-upload="beforeUpload"
      :before-remove="beforeRemove">
      <!--自定义默认内容 - 上传按钮-->
      <template #default>
        <!--拖拽上传-->
        <template v-if="drag">
          <div class="drag-content">
            <slot>
              <def-icon class="el-icon--upload" icon="uploadFilled" />
              <div class="el-upload__text">
                <template v-if="listType !== 'picture-card'">
                  拖拽文件到这里 或 <em>点击上传</em>
                </template>
                <template v-else> 拖拽文件 <em>点击上传</em> </template>
              </div>
            </slot>
          </div>
        </template>
        <!--非拖拽上传-->
        <template v-else>
          <slot>
            <template v-if="listType !== 'picture-card'">
              <el-button type="primary">Click to upload</el-button>
            </template>
            <template v-else>
              <def-icon icon="plus" />
            </template>
          </slot>
        </template>
      </template>
      <!--提示说明文字-->
      <template #tip v-if="!hiddenTip">
        <div class="tip el-upload__tip">
          <slot name="tip">
            {{ tip }}
          </slot>
        </div>
      </template>
    </el-upload>
    <!--图片预览-->
    <el-dialog v-if="props.listType === 'picture-card'" v-model="dialogVisible">
      <img class="preview-img" :src="previewImgUrl" alt="预览图片" />
    </el-dialog>
  </div>
</template>

<script lang="ts" setup>
import { ref, PropType, watch } from 'vue'
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
import type {
  UploadProps,
  UploadFile,
  UploadFiles,
  UploadRawFile,
  UploadUserFile,
  UploadProgressEvent
} from 'element-plus'
import { Awaitable } from 'element-plus/es/utils'
import { TElUploadListType } from './types'
import NProgress from '@/plugins/NProgress' // 引入顶部进度条插件

/* 参数声明 */
const props = defineProps({
  fileList: {
    // 文件列表
    type: Array as PropType<UploadUserFile[]>,
    default: () => []
  },
  action: {
    // 上传地址
    type: String,
    default: process.env.VUE_APP_FILE_URL
  },
  multiple: {
    // 是否支持多选文件
    type: Boolean,
    default: true
  },
  limit: Number, // 允许上传文件的最大数量
  disabled: {
    // 是否禁用上传
    type: Boolean,
    default: false
  },
  showFileList: {
    // 是否显示已上传文件列表
    type: Boolean,
    default: true
  },
  listType: {
    // 文件列表的类型
    type: String as PropType<TElUploadListType>,
    default: 'text'
  },
  drag: {
    // 是否启用拖拽上传
    type: Boolean,
    default: true
  },
  // 自定义属性
  hiddenTip: {
    // 是否隐藏tip
    type: Boolean,
    default: true
  },
  tip: {
    // tip 内容
    type: String,
    default: '自定义提示tip'
  },
  hiddenLoading: {
    // 上传文件时是否隐藏loading效果
    type: Boolean,
    default: false
  },
  size: {
    // 上传文件限制大小 默认1GB = 1024MB
    type: Number,
    default: 1024
  },
  compress: {
    // 图片压缩
    type: Boolean,
    default: false
  },
  compressWidth: Number // 图片压缩宽度
})
const emits = defineEmits(['update:fileList'])

/* 数据处理 */
const fileListVal = ref<UploadUserFile[]>(props.fileList)
const disabledVal = ref<boolean>(props.disabled)
const loading = ref<any>()
const imageType = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'apng']

watch(
  () => props.disabled,
  (val) => {
    disabledVal.value = val
  }
)
/**
 * 图片压缩
 * @param file：需要压缩的文件对象
 * @param compressWidth：需要压缩的宽度
 * @description
 *  1、将文件转img对象
 *  2、获取文件宽高比例
 *  3、自定义等比例缩放宽高属性，这里我用的是传入compressWidth或默认图片原宽度，高度是等比例缩放
 *  4、canvas重新绘制图片
 *  5、canvas转二进制对象转文件对象，返回
 * @returns { File } 文件
 */
const imgCompress = async (file: UploadRawFile, compressWidth?: number) => {
  // 将文件转img对象
  const img: any = await fileToImg(file)
  return new Promise((resolve) => {
    const canvas = document.createElement('canvas')
    const context: CanvasRenderingContext2D = canvas.getContext('2d')!
    // 获取文件宽高比例
    const { width: originWidth, height: originHeight } = img
    // 自定义等比例缩放宽高属性，这里我用的是传入compressWidth或默认图片原宽度，高度是等比例缩放
    const scale = +(originWidth / originHeight).toFixed(2) // 比例取小数点后两位)
    let targetWidth = compressWidth || originWidth // 固定宽
    if (originWidth > 1920) targetWidth = 1920 // 如果图片宽超过1920 等比压缩
    const targetHeight = Math.round(targetWidth / scale) // 等比例缩放高
    canvas.width = targetWidth
    canvas.height = targetHeight
    context.clearRect(0, 0, targetWidth, targetHeight)
    // canvas重新绘制图片
    context.drawImage(img, 0, 0, targetWidth, targetHeight)
    // canvas转二进制对象转文件对象，返回
    // const compressImage = canvas.toDataURL(file.type || 'image/jpeg', 0.9)
    // resolve(compressImage)
    const type = file.type || 'image/jpeg'
    canvas.toBlob(function (blob: any) {
      const f = new File([blob], file.name, {
        type,
        lastModified: file.lastModified
      })
      resolve(f)
    }, type)
  })
}
// file转换成img对象
const fileToImg = (file: UploadRawFile) => {
  return new Promise((resolve, reject) => {
    const img: any = new Image()
    const reader = new FileReader()
    reader.onload = function (e: any) {
      img.src = e.target.result
    }
    reader.onerror = function (e) {
      reject(e)
    }
    reader.readAsDataURL(file)
    img.onload = function () {
      resolve(img)
    }
    img.onerror = function (e: any) {
      reject(e)
    }
  })
}

// 点击文件列表中已上传的文件时的钩子
const dialogVisible = ref(false)
const previewImgUrl = ref('')
watch(
  () => dialogVisible.value,
  (val) => {
    console.log(val)
  }
)
const onPreview: UploadProps['onPreview'] = (uploadFile: UploadFile): void => {
  if (props.listType === 'picture-card') {
    // 如果为picture-card类型 才预览
    dialogVisible.value = true
    previewImgUrl.value = uploadFile.url!
  }
}
// 上传文件之前的钩子，参数为上传的文件， 若返回false或者返回 Promise 且被 reject，则停止上传。
const beforeUpload: UploadProps['beforeUpload'] = (
  rawFile: UploadRawFile
): Awaitable<void | undefined | null | boolean | File | Blob> => {
  // 上传大小限制
  const imageSizeM = rawFile.size / 1024 / 1024 // 1MB
  if (imageSizeM > props.size) {
    ElMessage.warning('文件大小不能超过' + props.size + 'MB')
    return false
  }
  // 上传图片压缩
  const isImage =
    rawFile.type.startsWith('image/') &&
    imageType.includes(rawFile.type.split('/')[1].toLowerCase())
  return new Promise((resolve, reject) => {
    if (isImage) {
      if (props.compress) {
        // 图片是否压缩
        imgCompress(rawFile, props.compressWidth).then((file: any) => {
          console.log('压缩后的图片文件对象', file)
          resolve(file)
        })
      } else {
        resolve(rawFile)
      }
    } else {
      // 非图片文件
      ElMessage.warning('请上传图片')
      reject(new Error('请上传图片'))
    }
  })
}
// 文件状态改变时的钩子，添加文件、上传成功和上传失败时都会被调用
const onChange: UploadProps['onChange'] = (
  uploadFile: UploadFile,
  uploadFiles: UploadFiles
): void => {
  console.log('onChange:', uploadFile, uploadFiles)
}
// 文件上传时的钩子
const onProgress: UploadProps['onProgress'] = (
  evt: UploadProgressEvent,
  uploadFile: UploadFile,
  uploadFiles: UploadFiles
): void => {
  // 上传进度
  console.log('onProgress:', evt, uploadFile, uploadFiles)
  // 开启进度条
  NProgress.start()
  // 开启loading效果
  if (!props.hiddenLoading) {
    loading.value = ElLoading.service({
      lock: true,
      text: '上传中...',
      background: 'rgba(0, 0, 0, 0.7)'
    })
  }
}
// 当超出限制时，执行的钩子函数
const onExceed: UploadProps['onExceed'] = (
  files: File[],
  uploadFiles: UploadFiles | UploadUserFile[]
): void => {
  console.log('onExceed:', files, uploadFiles)
  ElMessage.warning(
    `上传数量限制为${props.limit}个文件，你这次选择了${files.length}个文件，加起来总的数量为${
      files.length + uploadFiles.length
    }个文件`
  )
}
// 文件上传成功时的钩子
const onSuccess: UploadProps['onSuccess'] = (
  response: any,
  uploadFile: UploadFile,
  uploadFiles: UploadFiles
): void => {
  console.log('onSuccess:', response, uploadFile, uploadFiles)
  emits('update:fileList', uploadFiles)
  // 关闭进度条
  NProgress.done()
  // 关闭loading
  if (!props.hiddenLoading) {
    loading.value.close()
  }
}
// 文件上传失败时的钩子
const onError: UploadProps['onError'] = (
  error: Error,
  uploadFile: UploadFile,
  uploadFiles: UploadFiles
): void => {
  console.log('onError:', error, uploadFile, uploadFiles)
  ElMessage.error('上传文件失败, 错误原因：' + error.message)
  // 关闭进度条
  NProgress.done()
  // 关闭loading
  if (!props.hiddenLoading) {
    loading.value.close()
  }
}
// 删除文件之前的钩子，参数为上传的文件和文件列表， 若返回 false 或者返回 Promise 且被 reject，则停止删除。
const beforeRemove: UploadProps['beforeRemove'] = (
  uploadFile: UploadFile,
  uploadFiles: UploadFiles
): Awaitable<boolean> => {
  console.log('beforeRemove:', uploadFile, uploadFiles)
  if (uploadFile.status !== 'success') return true
  return ElMessageBox.confirm(`确认移除文件名为 ${uploadFile.name} 的文件?`).then(
    () => true,
    () => false
  )
}
// 文件列表移除文件时的钩子
const onRemove: UploadProps['onRemove'] = (
  uploadFile: UploadFile,
  uploadFiles: UploadFiles
): void => {
  console.log('onRemove:', uploadFile, uploadFiles)
  emits('update:fileList', uploadFiles)
}
// 清空文件列表
const clear = () => {
  emits('update:fileList', [])
}
defineExpose({
  clear
})
</script>

<style scoped lang="scss">
.def-upload {
  :deep(.el-upload-list) {
    height: 100%;
  }
  :deep(.el-upload) {
    height: 100%;
    .el-upload-dragger {
      height: 100%;
    }
  }
  .drag-content {
    @include posCenterXY;
    top: 45%;
  }
  .preview-img {
    width: 100%;
  }

  // 解决按钮点击后移开鼠标颜色仍然聚焦的解决方法
  :deep(.el-button) {
    &:focus {
      color: var(--el-button-text-color);
      background-color: var(--el-button-bg-color);
      border: var(--el-border);
      border-color: var(--el-button-border-color);
    }
    &:hover {
      color: var(--el-button-hover-text-color);
      background-color: var(--el-button-hover-bg-color);
      border-color: var(--el-button-hover-border-color);
      outline: 0;
    }
  }
}
</style>
