<script setup lang="ts">
import type { UploadFile, UploadFiles, UploadProgressEvent, UploadRawFile, UploadRequestOptions, UploadStatus } from "element-plus"
import { Close, Plus, VideoCamera, View } from "@element-plus/icons-vue"
import { ElMessage, ElNotification, useFormItem } from "element-plus"
import { defineEmits, defineProps, ref, watch } from "vue"
import { useI18n } from "vue-i18n"
import { uploadFile } from "@/api/common"

// 定义类型
type CustomUploadFile = {
  type?: "image" | "video" | "both"
  url: string
  status?: UploadStatus
} & Partial<UploadFile>

interface UploadProps {
  multiple?: boolean
  data?: Record<string, any>
  qureryType?: string
  disabled?: boolean
  fileType?: "image" | "video" | "both"
  max?: number | string
  modelValue?: CustomUploadFile[]
  action?: string
  name?: string
  maxSize?: number
  handleImgSize?: (img: HTMLImageElement) => boolean
}

// 定义属性
const props = withDefaults(defineProps<UploadProps>(), {
  multiple: false,
  data: () => ({}),
  qureryType: "",
  disabled: false,
  fileType: "image",
  max: 1,
  modelValue: () => [],
  action: "/upload",
  name: "file",
  maxSize: 5120, // 5M
  handleImgSize: undefined
})

// 定义事件
const emit = defineEmits<{
  "update:modelValue": [value: { url: string }[]]
  "change": [value: { url: string }[], responseData?: any]
}>()

const { t } = useI18n()

const testVideo = /\.mp4$/
const imageFromat = ["jpg", "jpeg", "png"]
const imageAccept = "image/jpg, image/jpeg, image/png"
const videoFromat = ["mp4"]
const videoAccept = "video/mp4"
const fileTypes = {
  image: {
    format: imageFromat,
    accept: imageAccept,
    list: imageAccept.split(", ")
  },
  video: {
    format: videoFromat,
    accept: videoAccept,
    list: videoAccept.split(", ")
  },
  both: {
    format: imageFromat.concat(videoFromat),
    accept: `${imageAccept}, ${videoAccept}`,
    list: [...imageAccept.split(", "), ...videoAccept.split(", ")]
  }
}

function getFileType(url: string): "image" | "video" {
  return testVideo.test(url) ? "video" : "image"
}

function getItem(item: CustomUploadFile): CustomUploadFile {
  return {
    url: item.url,
    status: "success",
    type: item.type ?? getFileType(item.url ?? "")
  }
}

// 响应式数据
const actionBaseUrl = ref("")
const fileList = ref<CustomUploadFile[]>([])

// 监听器
watch(
  () => props.modelValue,
  (val) => {
    if (val) {
      fileList.value = val.map(getItem)
    }
  },
  { deep: true, immediate: true }
)

// 方法
function handleRemove(index: number) {
  console.log("remove")
  fileList.value.splice(index, 1)
  updateValue()
}

function handleProgress(evt: UploadProgressEvent, uploadFile: UploadFile, uploadFiles: UploadFiles) {
  fileList.value = uploadFiles as CustomUploadFile[]
}

//  表单校验上下文
const { formItem } = useFormItem()
function updateValue(responseData?: any) {
  const files = fileList.value.map((item) => {
    return {
      url: item.url ?? ""
    }
  })
  console.log(fileList.value, "change form item")
  emit("update:modelValue", files)
  emit("change", files, responseData)
  formItem?.validate("change")
}

function httpRequest(param: UploadRequestOptions) {
  const file = param.file
  // 发送请求的参数格式为FormData
  const formData = new FormData()
  formData.append("file", file)
  // 调用param中的钩子函数处理各种情况，这样就可以用在组件中用钩子了。也可以用res.code==200来进行判断处理各种情况
  return uploadFile(formData, param)
    .then((res) => {
      param.onSuccess(res)
    })
    .catch((err) => {
      param.onError(err)
    })
}

function handleSuccess(res: any, file: UploadFile, fileListParam: UploadFiles) {
  console.log(fileListParam)
  console.log(res, file)
  if (res.code === 0) {
    const currentFile = fileListParam.find(item => item.uid === file.uid)
    if (currentFile) {
      currentFile.url = res.data
    }
    fileList.value = fileListParam as CustomUploadFile[]
    updateValue(res.data)
  } else {
    // 上传失败，清除progress中的文件
    const findIndex = fileList.value.findIndex(item => item.uid === file.uid)
    if (findIndex > -1) {
      fileList.value.splice(findIndex, 1)
    }
    console.log("errror")
    ElMessage({ type: "error", message: res.msg || t("common.upload.fail"), duration: 3000 })
  }
}

function handleError(error: Error, file: UploadFile) {
  console.log("error", error, file)
  // 上传失败，清除progress中的文件
  const findIndex = fileList.value.findIndex(item => item.uid === file.uid)
  if (findIndex > -1) {
    fileList.value.splice(findIndex, 1)
  }
  if (error && JSON.stringify(error).indexOf("401") > 1) {
    ElMessage({
      type: "error",
      message: t("common.upload.token"),
      duration: 3000
    })
    return
  }
  ElMessage({ type: "error", message: t("common.upload.fail"), duration: 3000 })
}

function handleFormatError(file: CustomUploadFile) {
  ElNotification.warning({
    title: t("common.upload.fileFormat"),
    message: t("common.upload.fileFormat", { name: (file.raw as File)?.name })
  })
}

function handleMaxSize(file: UploadRawFile): boolean {
  if (props.maxSize) {
    console.log((file.size ?? 0) / 1024)
    const isMax = (file.size ?? 0) / 1024 > Number(props.maxSize)
    if (isMax) {
      ElNotification.warning({
        title: t("common.upload.maxTitle"),
        message: t("common.upload.max", {
          name: file.name,
          size: (props.maxSize / 1024).toFixed(2)
        })
      })
      return false
    }
  }
  return true
}

// 超出文件个数
function handleMoreMaxFile() {
  ElNotification.warning({
    title: t("common.upload.maxCount", { maxCount: props.max })
  })
}

// 根据props中的fileType选择对应的类型数组
function handleTypeValid(file: UploadRawFile): boolean {
  const { type } = file
  return fileTypes[props.fileType]?.list.includes(type) ?? true
}

async function handleBeforeUpload(file: UploadRawFile): Promise<boolean> {
  if (!handleTypeValid(file)) {
    return Promise.reject(new Error("fileType error"))
  }
  if (props.handleImgSize) {
    const res = await asyncImgChecked(file)
    // 校验不通过不上传
    if (!res) {
      return Promise.reject(new Error("handleImgSize error"))
    }
  }
  if (!handleMaxSize(file)) {
    console.log("before upload max")
    return Promise.reject(new Error("maxSize error"))
  }

  const check = fileList.value.length < Number(props.max)
  if (!check) {
    ElNotification.warning({
      title: t("common.upload.maxCount", { maxCount: props.max })
    })
  }
  return check ? Promise.resolve(true) : Promise.reject(new Error("over max"))
}

function asyncImgChecked(file: UploadRawFile): Promise<boolean> {
  return new Promise((resolve) => {
    const reader = new FileReader()
    reader.readAsDataURL(new Blob([file])) // 必须用file.raw
    reader.onload = () => {
      // 让页面中的img标签的src指向读取的路径
      const img = new Image()
      img.src = reader.result as string
      if (img.complete) {
        // 如果存在浏览器缓存中
        resolve(props.handleImgSize!(img))
      } else {
        img.onload = () => {
          resolve(props.handleImgSize!(img))
        }
      }
    }
  })
}

function resetCustomUploadFile() {
  fileList.value = []
  updateValue()
}

const showPreview = ref(false)
const previewUrl = ref<string[]>([])
function handlePreview(item: CustomUploadFile) {
  showPreview.value = true
  previewUrl.value = [item.url]
}

defineExpose({
  resetCustomUploadFile
})
</script>

<template>
  <div class="upload-file-wrapper">
    <slot name="uploader-before" />

    <div v-for="(item, index) in fileList" :key="item.uid || index" class="file-upload-list">
      <template v-if="item.status === 'success'">
        <img v-if="item.type === 'image'" class="img-item" :src="item.url">
        <el-icon v-else-if="item.type === 'video'">
          <VideoCamera />
        </el-icon>
        <div class="file-upload-list-cover" :class="{ 'is-disabled': disabled }">
          <el-icon @click="handlePreview(item)">
            <View />
          </el-icon>
          <el-icon v-if="!disabled" class="el-icon-close" @click="handleRemove(index)">
            <Close />
          </el-icon>
        </div>
      </template>
      <template v-else>
        <el-progress class="progress" :percentage="item.percentage" />
      </template>
    </div>
    <el-upload
      v-show="fileList.length < Number(max) && !disabled"
      class="uploader" :class="{ 'upload-disabled': disabled }"
      list-type="picture-card"
      :disabled="disabled"
      :show-file-list="false"
      :http-request="httpRequest"
      :on-progress="handleProgress"
      :on-success="handleSuccess"
      :on-error="handleError"
      :format="fileTypes[fileType].format"
      :accept="fileTypes[fileType].accept"
      :max-size="maxSize"
      :on-format-error="handleFormatError"
      :on-exceeded-size="handleMoreMaxFile"
      :before-upload="handleBeforeUpload"
      :multiple="multiple"
      :data="data"
      :file-list="fileList as UploadFiles"
      :action="`${actionBaseUrl}${action}`"
      type="drag"
    >
      <slot name="upload-content">
        <el-icon class="el-icon-plus">
          <Plus />
        </el-icon>
      </slot>
    </el-upload>
    <slot name="uploader-after" />
    <el-image-viewer
      v-if="showPreview"
      :url-list="previewUrl"
      @close="showPreview = false"
    />
  </div>
</template>

<style lang="scss" scoped>
$width: 240px;
$height: 150px;
.upload-file-wrapper {
  display: flex;
  line-height: 0;
  margin-right: 8px;
  flex-wrap: wrap;
  .add-btn {
    width: $width - 2;
    height: $height - 2;
    line-height: $height;
  }
  .simple-i {
    font-size: 26px;
  }
}

.uploader {
  display: inline-block;
  margin-right: 15px;
  :deep(.el-upload--picture-card) {
    display: flex;
    align-items: center;
    justify-content: center;
    width: $width;
    height: $height;
    border: 1px solid #dddfe7;
    background-color: #fff;
  }
  .el-icon-plus {
    font-size: 22px;
  }
  .el-upload {
    border: 1px solid #dddfe7;
    border-radius: 4px;
    cursor: pointer;
    position: relative;
    &:hover {
      border-color: $theme-color;
    }
  }
}

.upload-disabled {
  :deep(.el-upload--picture-card:hover) {
    border-color: #c0ccda;
    color: unset;
    cursor: not-allowed;
  }
}

.progress {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  transform: translateY(-50%);
}
.file-upload-list {
  display: inline-block;
  width: $width;
  height: $height;
  text-align: center;
  line-height: $height;
  border-radius: 4px;
  margin-right: 15px;
  margin-bottom: 12px;
  overflow: hidden;
  background: #fff;
  position: relative;
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
  img {
    width: 100%;
    height: 100%;
  }
  &:hover .file-upload-list-cover {
    display: block;
  }
}
.file-upload-list-cover {
  display: none;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  background: rgba(0, 0, 0, 0.5);
  &.is-disabled {
    i {
      margin-right: 0;
    }
  }
  i {
    color: #fff;
    font-size: 16px;
    cursor: pointer;
    margin-right: 33px;
  }
  .el-icon-close {
    margin-right: 0;
  }
}

.img-item {
  object-fit: cover;
}

.mobile {
  .file-upload-list {
    margin-bottom: 12px;
  }
}
</style>
