image.vue 7.37 KB
<template>
  <div class="checkin-upload-image p-4">
    <!-- 图片上传区域 -->
    <div class="mb-4">
      <van-uploader
        v-model="fileList"
        :max-count="max_count"
        :max-size="20 * 1024 * 1024"
        :before-read="beforeRead"
        :after-read="afterRead"
        @delete="onDelete"
        multiple
        accept="image/*"
        result-type="file"
      >
        <template #upload-text>
          <div class="text-center">
            <van-icon name="photograph" size="24" />
            <div class="mt-1 text-sm text-gray-600">上传图片</div>
          </div>
        </template>
      </van-uploader>
      <div class="mt-2 text-xs text-gray-500">最多上传{{ max_count }}张图片,每张不超过20M</div>
      <div class="mt-2 text-xs text-gray-500">上传类型:&nbsp;{{ type_text }}</div>
    </div>

    <!-- 文字留言区域 -->
    <div class="mb-4 border">
      <van-field
        v-model="message"
        rows="4"
        autosize
        type="textarea"
        placeholder="请输入打卡留言"
      />
    </div>

    <!-- 提交按钮 -->
    <div class="fixed bottom-0 left-0 right-0 p-4 bg-white">
      <van-button
        type="primary"
        block
        :loading="uploading"
        :disabled="!canSubmit"
        @click="onSubmit"
      >
        提交
      </van-button>
    </div>

    <!-- 上传加载遮罩 -->
    <van-overlay :show="loading">
      <div class="wrapper" @click.stop>
        <van-loading vertical color="#FFFFFF">上传中...</van-loading>
      </div>
    </van-overlay>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { showToast, showLoadingToast } from 'vant'
import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common'
import BMF from 'browser-md5-file'
import _ from 'lodash'
import { useTitle } from '@vueuse/core';

const route = useRoute()
const router = useRouter()
useTitle(route.meta.title);

const max_count = ref(5);

// 文件列表
const fileList = ref([])
// 留言内容
const message = ref('')
// 上传状态
const uploading = ref(false)
// 上传loading
const loading = ref(false)

// 是否可以提交
const canSubmit = computed(() => {
  return fileList.value.length > 0 && message.value.trim() !== ''
})

// 固定类型限制
const imageTypes = "jpg/jpeg/png";

// 文件类型中文页面显示
const type_text = computed(() => {
  // return props.item.component_props.image_type;
  return imageTypes;
});

// 文件校验
const beforeRead = (file) => {
  const image_types = _.map(imageTypes.split("/"), (item) => `image/${item}`);
  let flag = true

  if (Array.isArray(file)) {
    // 多张图片
    const invalidTypes = file.filter(item => {
      const fileType = item.type.toLowerCase();
      return !image_types.some(type => fileType.includes(type.split('/')[1]));
    })
    if (invalidTypes.length) {
      flag = false
      showToast('请上传指定格式图片')
    }
    if (fileList.value.length + file.length > max_count.value) {
      flag = false
      showToast(`最大上传数量为${max_count.value}张`)
    }
  } else {
    const fileType = file.type.toLowerCase();
    if (!image_types.some(type => fileType.includes(type.split('/')[1]))) {
      showToast('请上传指定格式图片')
      flag = false
    }
    if (fileList.value.length + 1 > max_count.value) {
      flag = false
      showToast(`最大上传数量为${max_count.value}张`)
    }
    if ((file.size / 1024 / 1024).toFixed(2) > 20) {
      flag = false
      showToast('最大文件体积为20MB')
    }
  }
  return flag
}

// 获取文件MD5
const getFileMD5 = (file) => {
  return new Promise((resolve, reject) => {
    const bmf = new BMF()
    bmf.md5(file, (err, md5) => {
      if (err) {
        reject(err)
        return
      }
      resolve(md5)
    })
  })
}

// 上传到七牛云
const uploadToQiniu = async (file, token, fileName) => {
  const formData = new FormData()
  formData.append('file', file)
  formData.append('token', token)
  formData.append('key', fileName)

  const config = {
    headers: { 'Content-Type': 'multipart/form-data' }
  }

  // 根据协议选择上传地址
  const qiniuUploadUrl = window.location.protocol === 'https:'
    ? 'https://up.qbox.me'
    : 'http://upload.qiniu.com'

  return await qiniuUploadAPI(qiniuUploadUrl, formData, config)
}

// 处理单个文件上传
const handleUpload = async (file) => {
  loading.value = true
  try {
    // 获取MD5值
    const md5 = await getFileMD5(file.file)

    // 获取七牛token
    const tokenResult = await qiniuTokenAPI({
      name: file.file.name,
      hash: md5
    })

    // 文件已存在,直接返回
    if (tokenResult.data) {
      return tokenResult.data
    }

    // 新文件上传
    if (tokenResult.token) {
      const suffix = /.[^.]+$/.exec(file.file.name) || ''
      const fileName = `uploadForm/${route.query.code || 'checkin'}/${md5}${suffix}`

      const { filekey, image_info } = await uploadToQiniu(
        file.file,
        tokenResult.token,
        fileName
      )

      if (filekey) {
        // 保存文件信息
        const { data } = await saveFileAPI({
          name: file.file.name,
          filekey,
          hash: md5,
          height: image_info?.height,
          width: image_info?.width
        })
        return data
      }
    }
    return null
  } catch (error) {
    console.error('Upload error:', error)
    return null
  } finally {
    loading.value = false
  }
}

// 文件读取后的处理
const afterRead = async (file) => {
  if (Array.isArray(file)) {
    // 多文件上传
    for (const item of file) {
      item.status = 'uploading'
      item.message = '上传中...'
      const result = await handleUpload(item)
      if (result) {
        item.status = 'done'
        item.message = '上传成功'
        item.url = result.url
      } else {
        item.status = 'failed'
        item.message = '上传失败'
        showToast('上传失败,请重试')
      }
    }
  } else {
    // 单文件上传
    file.status = 'uploading'
    file.message = '上传中...'
    const result = await handleUpload(file)
    if (result) {
      file.status = 'done'
      file.message = '上传成功'
      file.url = result.url
    } else {
      file.status = 'failed'
      file.message = '上传失败'
      showToast('上传失败,请重试')
    }
  }
}

// 删除文件
const onDelete = (file) => {
  const index = fileList.value.indexOf(file)
  if (index !== -1) {
    fileList.value.splice(index, 1)
  }
}

// 提交表单
const onSubmit = async () => {
  if (uploading.value) return

  // 检查是否所有文件都上传完成
  const hasUploadingFiles = fileList.value.some(file => file.status === 'uploading')
  if (hasUploadingFiles) {
    showToast('请等待所有文件上传完成')
    return
  }

  uploading.value = true
  const toast = showLoadingToast({
    message: '提交中...',
    forbidClick: true,
  })

  try {
    // TODO: 调用提交打卡接口
    await new Promise(resolve => setTimeout(resolve, 1000))
    showToast('提交成功')
    router.back()
  } catch (error) {
    showToast('提交失败,请重试')
  } finally {
    toast.close()
    uploading.value = false
  }
}
</script>

<style lang="less" scoped>
.checkin-upload-image {
  min-height: 100vh;
  padding-bottom: 80px;
}

.wrapper {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}
</style>