video.vue 8.72 KB
<!--
 * @Date: 2025-06-03 09:41:41
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2025-07-02 18:33:29
 * @FilePath: /mlaj/src/views/checkin/upload/video.vue
 * @Description: 音视频文件上传组件
-->
<template>
  <div class="checkin-upload-file p-4">
    <div class="title text-center pb-4 font-bold">{{route.meta.title}}</div>
    <!-- 文件上传区域 -->
    <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="video/*"
        result-type="file"
        upload-icon="plus"
        :deletable="false"
      >
      </van-uploader>
      <van-row v-for="(item, index) in fileList" :key="index" class="mt-2 text-s text-gray-500">
        <van-col span="22">{{ item.name }}</van-col>
        <van-col span="2" @click="delItem(item)"><van-icon name="clear" /></van-col>
      </van-row>
      <van-divider />
      <div class="mt-2 text-xs text-gray-500">最多上传{{ max_count }}个文件,每个不超过20M</div>
      <div class="mt-2 text-xs text-gray-500">上传类型:&nbsp;视频文件</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 { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin";
import BMF from 'browser-md5-file'
import _ from 'lodash'
import { useTitle } from '@vueuse/core';
import { useAuth } from '@/contexts/auth'

const route = useRoute()
const router = useRouter()
const { currentUser } = useAuth()
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 beforeRead = (file) => {
  let flag = true

  if (Array.isArray(file)) {
    // 多个文件
    const invalidTypes = file.filter(item => {
      const fileType = item.type.toLowerCase();
      return !fileType.startsWith('video/');
    })
    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 (!fileType.startsWith('video/')) {
      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 = `mlaj/upload/checkin/${currentUser.value.mobile}/file/${md5}${suffix}`

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

      if (filekey) {
        // 保存文件信息
        const { data } = await saveFileAPI({
          name: file.file.name,
          filekey,
          hash: md5
        })
        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
        item.meta_id = result.meta_id
        item.name = result.name
      } 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
      file.meta_id = result.meta_id
      file.name = result.name
    } 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 {
    if (route.query.status === 'edit') {
      // 编辑打卡接口
      const { code, data } = await editUploadTaskInfoAPI({
        i: route.query.post_id,
        note: message.value,
        meta_id: fileList.value.map(item => item.meta_id),
        file_type: route.query.type,
      });
      if (code) {
        showToast('提交成功')
        router.back()
      }
    } else {
      // 新增打卡接口
      const { code, data } = await addUploadTaskAPI({
        task_id: route.query.id,
        note: message.value,
        meta_id: fileList.value.map(item => item.meta_id),
        file_type: route.query.type,
      });
      if (code) {
        showToast('提交成功')
        router.back()
      }
    }
  } catch (error) {
    showToast('提交失败,请重试')
  } finally {
    toast.close()
    uploading.value = false
  }
}

onMounted(async () => {
  if (route.query.status === 'edit') {
    const { code, data } = await getUploadTaskInfoAPI({ i: route.query.post_id });
    if (code) {
      fileList.value = data.files.map(item => ({
        url: item.value,
        status: 'done',
        message: '上传成功',
        meta_id: item.meta_id,
        name: item.name,
      }))
      message.value = data.note
    }
  }
});

const delItem = (item) => {
  const index = fileList.value.indexOf(item)
  if (index!== -1) {
    fileList.value.splice(index, 1)
  }
}
</script>

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

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