vue3 上传文件,图片,视频组件

上传文件

ini 复制代码
<!-- eslint-disable vue/multi-word-component-names -->
<template>
  <div class="upload-file">
    <el-upload
      ref="uploadRef"
      :multiple="true"
      :action="uploadFileUrl"
      :before-upload="handleBeforeUpload"
      v-model="fileList"
      :file-list="fileList"
      :limit="limit"
      :on-error="handleUploadError"
      :on-exceed="handleExceed"
      :on-success="handleUploadSuccess"
      :show-file-list="false"
      :headers="headers"
      :auto-upload="true"
      class="upload-file-uploader"
    >
      <!-- 上传按钮 -->
      <el-button type="primary" v-show="isShow">选取文件</el-button>
    </el-upload>
    <!-- 上传提示 -->
    <div class="el-upload__tip" v-if="showTip" v-show="isShow">
      请上传
      <template v-if="fileSize">
        大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
      </template>
      <template v-if="fileType">
        格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
      </template>
  的文件
</div>
<!-- 文件列表 -->
<transition-group
  class="upload-file-list el-upload-list el-upload-list--text"
  name="el-fade-in-linear"
  tag="ul"
>
  <li
    :key="file.uid"
    class="el-upload-list__item ele-upload-list__item-content"
    v-for="(file, index) in fileList"
  >
    <el-link :underline="false" target="_blank">
      <span class="el-icon-document">
        {{ file.attachmentName }}
      </span>
    </el-link>
    <div class="ele-upload-list__item-content-action">
      <el-link v-show="isShow" :underline="false" @click="handleDelete(index)" type="danger"
        >删除</el-link
      >
      <el-link
        :underline="false"
        type="primary"
        @click="downloadFile(file)"
        v-if="dowloadStatus"
        >下载</el-link
      >
    </div>
  </li>
</transition-group>
typescript 复制代码
<script lang="ts" setup>
import cache from '@/utils/cache';
import { log } from 'console';
import { ElMessage, UploadUserFile } from 'element-plus';
import { ref, computed, watch } from 'vue';
import { Download } from '@element-plus/icons-vue';
import { useUserStore } from '@/stores';
const uploadRef = ref();

const props = defineProps({
  modelValue: [String, Object, Array],
  // 数量限制
  limit: {
    type: Number,
    default: 5
  },
  // 大小限制(MB)
  fileSize: {
    type: Number,
    default: 1100
  },
  // 文件类型, 例如['png', 'jpg', 'jpeg']
  fileType: {
    type: Array,
    default: () => ['doc', 'xls', 'xlsx', 'pdf', 'docx']
  },
  // 是否显示提示
  isShowTip: {
    type: Boolean,
    default: true
  },
  //是否显示删除按钮
  isShow: {
    type: Boolean,
    default: true
  },
  //是否显示下载
  dowloadStatus: {
    type: Boolean,
    default: false
  }
});

// @ts-ignore
const { proxy } = getCurrentInstance();
// eslint-disable-next-line vue/valid-define-emits
const emit = defineEmits();
const number = ref(0);
const uploadFileUrl = import.meta.env.VITE_BASE_API + '/minio/upload'; // 上传文件服务器地址
const headers = ref({
  Authorization: 'Bearer ' + useUserStore().token,
  'bg-debug': 1
});
const fileList = ref<UploadUserFile[]>([]);
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));

watch(
  [() => props.modelValue, () => props.dowloadStatus],
  (val: any) => {
    if (val) {
      fileList.value = props.modelValue;
    }
  },
  { deep: true, immediate: true }
);
// 上传前校检格式和大小
function handleBeforeUpload(file: { name: string; size: number }) {
  // 校检文件类型
  if (props.fileType.length) {
    const fileName = file.name.split('.');
    const fileExt = fileName[fileName.length - 1];
    const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
    if (!isTypeOk) {
      ElMessage.error(`文件格式不正确, 请上传${props.fileType.join('/')}格式文件!`);
      return false;
    }
  }
  // 校检文件大小
  if (props.fileSize) {
    const isLt = file.size / 1024 / 1024 < props.fileSize;
    if (!isLt) {
      ElMessage.error(`上传文件大小不能超过 ${props.fileSize} MB!`);
      return false;
    }
  }
  number.value++;
  return true;
}

//下载文件
const downloadPdf = (data: any) => {
  const fileName = data.attachmentName;
  const fileUrl = data.attachmentPath;
  const request = new XMLHttpRequest();
  request.responseType = 'blob';
  request.open('Get', fileUrl);
  request.onload = () => {
    const url = window.URL.createObjectURL(request.response);
    const a = document.createElement('a');
    document.body.appendChild(a);
    a.href = url;
    a.download = fileName;
    a.click();
  };
  request.send();
};

//下载文件
const downloadFile = (file: { attachmentPath: any; attachmentName: any }) => {
  const lastDotIdx = file.attachmentPath.lastIndexOf('.');
  const type = file.attachmentPath.slice(lastDotIdx + 1).toUpperCase();
  if (type === 'PDF') {
    downloadPdf(file);
  } else {
    const link = document.createElement('a');
    link.href = file.attachmentPath;
    link.download = file.attachmentName;
    document.body.appendChild(link);
    link.click();
  }
};
// 文件个数超出
function handleExceed() {
  ElMessage.error(`上传文件数量不能超过 ${props.limit} 个!`);
}

// 上传失败
function handleUploadError(err: any) {
  ElMessage.error('上传文件失败');
}
/** 文件上传成功处理 */
const handleUploadSuccess: UploadProps['onSuccess'] = (
  response: { data: { url: any } },
  file: { name: any }
) => {
  const newFile = { attachmentName: file.name, attachmentPath: response.data.url };
  fileList.value.push(newFile);
  uploadRef.value.submit();
  emit('update:modelValue', fileList.value);
};
// 删除文件
function handleDelete(index: number) {
  fileList.value.splice(index, 1);
  // @ts-ignore
  emit('update:modelValue', fileList.value);
}
</script>

<style scoped lang="scss">
.upload-file-uploader {
  margin-bottom: 5px;
}
.upload-file-list .el-upload-list__item {
  border: 1px solid #e4e7ed;
  line-height: 2;
  margin-bottom: 10px;
  position: relative;
}
.upload-file-list .ele-upload-list__item-content {
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: inherit;
}
.ele-upload-list__item-content-action .el-link {
  margin-right: 10px;
  margin-left: 20px;
}
.ele-upload-list__item-content-action .el-icon {
  margin-right: 10px;
  margin-top: 10px;
}
</style>

上传图片

xml 复制代码
<template>
  <div class="pro-upload-img-box">
    <div class="pro-upload-img-content">
      <!-- 已上传图片列表 -->
      <div
        class="upload-img-card"
        v-for="(item, index) in fileList"
        :key="index"
      >
        <!-- 图片预览 -->
        <el-image
          class="img-sty"
          :preview-src-list="[item.url]"
          fit="cover"
          :src="item.url"
          alt=""
        />
        <!-- 删除按钮 -->
        <el-image
          v-if="!disabled"
          src="https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/img-close.png"
          class="img-close"
          @click="handleRemove(item, index)"
        />
        <!-- 图片遮罩层 -->
        <div class="img-mask">
          <el-image
            src="https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/img-preview.png"
            class="img-preview"
          />
        </div>
      </div>
      <!-- 上传组件 -->
      <el-upload
        v-loading="loading"
        ref="proUploadImgRef"
        :class="['pro-upload-img', { 'is-disabled': disabled }]"
        v-bind="uploadProps"
        :before-upload="beforeUpload"
        :on-success="handleSuccess"
        :on-error="handleError"
        :on-exceed="handleExceed"
      >
        <slot>
          <div class="upload-card">
            <el-icon class="upload-icon" style="font-size: 30px;">
              <CirclePlus  />
            </el-icon>
            <div v-if="uploadText" class="upload-text">
              {{ uploadText }}
            </div>
          </div>
        </slot>
      </el-upload>
    </div>
    <!-- 提示信息 -->
    <slot name="tip">
      <div class="upload-tip" v-if="tip">
        {{ tip }}
      </div>
    </slot>
  </div>
</template>

<script setup name="ProUploadImg">
  import { ref, computed } from 'vue';
  import { Plus } from '@element-plus/icons-vue';
  import { ElMessage } from 'element-plus';

  // Props 定义
  const props = defineProps({
    /** 上传地址 */
    action: {
      type: String,
      required: true,
    },
    /** 请求头 */
    headers: {
      type: Object,
      default: () => ({}),
    },
    /** 是否支持多选 */
    multiple: {
      type: Boolean,
      default: false,
    },
    /** 最大上传数量,0表示不限制 */
    limit: {
      type: Number,
      default: 0,
    },
    /** 接受的文件类型,如:.jpg,.png,.jpeg */
    accept: {
      type: String,
      default: '.jpg,.png,.jpeg',
    },
    /** 文件大小限制 */
    maxSize: {
      type: Number,
      default: 0,
    },
    /** 文件大小单位(KB/MB) */
    sizeUnit: {
      type: String,
      default: 'MB',
      validator: (value) => ['KB', 'MB'].includes(value),
    },
    /** 图片宽度限制 */
    width: {
      type: Number,
      default: 0,
    },
    /** 图片高度限制 */
    height: {
      type: Number,
      default: 0,
    },
    /** 上传提示文字 */
    uploadText: {
      type: String,
      default: '点击上传',
    },
    /** 上传提示说明 */
    tip: {
      type: String,
      default: '',
    },
    /** 是否禁用 */
    disabled: {
      type: Boolean,
      default: false,
    },
  });
  /** 初始文件列表 */
  const fileList = defineModel('fileList', {
    type: Array,
    default: () => [],
  });

  // 事件定义
  const emit = defineEmits(['success', 'error', 'exceed', 'remove']);

  const proUploadImgRef = ref();
  const loading = ref(false);

  const uploadProps = computed(() => ({
    action: props.action,
    accept: props.accept,
    limit: props.limit,
    multiple: props.multiple,
    listType: 'picture-card',
    showFileList: false,
    headers: props.headers,
    fileList: fileList.value,
    disabled: props.disabled,
  }));

  /**
   * 验证图片尺寸是否符合要求
   * @param {number} width - 图片宽度
   * @param {number} height - 图片高度
   * @returns {boolean} 是否符合要求
   */
  const validateImageSize = (width, height) => {
    if (props.width && props.height) {
      return width === props.width && height === props.height;
    }
    if (props.width) {
      return width === props.width;
    }
    if (props.height) {
      return height === props.height;
    }
    return true;
  };

  /**
   * 上传前校验
   * @param {File} file - 待上传的文件
   * @returns {Promise<boolean>} 是否通过校验
   */
  const beforeUpload = async (file) => {
    // 校验文件类型
    const fileTypeList = props.accept
      .split(',')
      .map((item) => item.replace('.', ''));
    const fileType = file.name.split('.').pop();

    if (!fileTypeList.includes(fileType)) {
      ElMessage({
        message: `仅支持 ${fileTypeList.join('、')} 格式`,
        type: 'warning',
      });
      return false;
    }

    // 校验文件大小
    if (props.maxSize) {
      const fileSize = file.size / 1024;
      const maxSizeInKB =
        props.sizeUnit === 'MB' ? props.maxSize * 1024 : props.maxSize;
      if (fileSize > maxSizeInKB) {
        ElMessage({
          message: `大小不能超过 ${props.maxSize}${props.sizeUnit}!`,
          type: 'warning',
        });
        return false;
      }
    }

    // 校验图片尺寸
    // return new Promise((resolve, reject) => {
    //   const img = new Image();
    //   img.src = URL.createObjectURL(file);
    //   img.onload = () => {
    //     URL.revokeObjectURL(img.src);
    //     const { width, height } = img;

    //     if (!validateImageSize(width, height)) {
    //       const message =
    //         props.width && props.height
    //           ? `图片尺寸必须为 ${props.width}x${props.height}`
    //           : props.width
    //             ? `图片宽度必须为 ${props.width}px`
    //             : `图片高度必须为 ${props.height}px`;

    //       ElMessage({
    //         message,
    //         type: 'warning',
    //       });
    //       reject(false);
    //       return;
    //     }
    //     loading.value = true;
    //     resolve(true);
    //   };
    //   img.onerror = () => {
    //     URL.revokeObjectURL(img.src);
    //     ElMessage({
    //       message: '图片加载失败',
    //       type: 'error',
    //     });
    //     reject(false);
    //   };
    // });
  };

  /**
   * 上传成功回调
   * @param {Object} response - 服务器响应数据
   * @param {Object} uploadFile - 上传文件对象
   * @param {Array} uploadFiles - 上传文件列表
   */
  const handleSuccess = (response, uploadFile, uploadFiles) => {
    console.log(response, uploadFile, uploadFiles,12345666)
    loading.value = false;
    if (response.code === 200) {
      fileList.value.push({ url: response.data.url });
      console.log(fileList.value,12345)
    } else {
      proUploadImgRef.value.handleRemove(uploadFile);
      ElMessage({
        message: response.msg || response.message || '上传失败',
        type: 'error',
      });
    }
    emit('success', response, uploadFile, uploadFiles);
  };

  /**
   * 上传失败回调
   * @param {Error} error - 错误信息
   * @param {Object} uploadFile - 上传文件对象
   * @param {Array} uploadFiles - 上传文件列表
   */
  const handleError = (error, uploadFile, uploadFiles) => {
    loading.value = false;
    ElMessage({
      message: '上传失败',
      type: 'error',
    });
    emit('error', error, uploadFile, uploadFiles);
  };

  /**
   * 超出限制回调
   * @param {Array} files - 超出限制的文件列表
   * @param {Array} uploadFiles - 已上传的文件列表
   */
  const handleExceed = (files, uploadFiles) => {
    ElMessage({
      message: `最多只能上传 ${props.limit} 张图片`,
      type: 'warning',
    });
    emit('exceed', files, uploadFiles);
  };

  /**
   * 移除图片
   * @param {Object} file - 要移除的文件对象
   * @param {number} index - 文件索引
   */
  const handleRemove = (file, index) => {
    fileList.value.splice(index, 1);
    proUploadImgRef.value.handleRemove(file);
    emit('remove', file);
  };
</script>

<style lang="scss" scoped>
.pro-upload-img-box {
  .pro-upload-img-content {
    display: flex;
    flex-wrap: wrap;
    // 已上传图片卡片样式
    .upload-img-card {
      width: 100px;
      height: 100px;
      position: relative;
      margin: 0 12px 12px 0;
      // 图片样式
      .img-sty {
        width: 100%;
        height: 100%;
        overflow: hidden;
        border-radius: 6px;
      }
      // 删除按钮样式
      .img-close {
        position: absolute;
        right: -6px;
        top: -6px;
        width: 20px;
        height: 20px;
        cursor: pointer;
        z-index: 2;
      }
      // 遮罩层样式
      .img-mask {
        background: rgba(0, 0, 0, 0.3);
        border-radius: 6px;
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        pointer-events: none;

        .img-preview {
          position: absolute;
          right: 8px;
          bottom: 8px;
          width: 20px;
          height: 20px;
          pointer-events: none;
        }
      }
    }

    // 禁用状态样式
    .is-disabled {
      :deep(.el-upload--picture-card) {
        cursor: not-allowed;
      }
    }
    // 上传按钮样式
    .pro-upload-img {
      margin-bottom: 12px;
      .upload-card {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        .upload-icon {
          font-size: 20px;
          color: #333;
          text-align: center;
          line-height: 100px;
        }

        .upload-text {
          line-height: 24px;
          color: #333;
          font-size: 14px;
          text-align: center;
          margin-top: 10px;
        }
      }
    }

    // 上传组件样式覆盖
    :deep(.el-upload--picture-card) {
      width: 100px;
      height: 100px;
      background-color: #F8F8F9;
    }
    :deep(.el-upload-list__item) {
      width: auto;
      height: auto;
      overflow: visible;
    }
  }
  // 提示文字样式
  .upload-tip {
    font-size: 12px;
    color: #909399;
  }
}
</style>

上传视频

xml 复制代码
<template>
  <div class="pro-upload-video-box">
    <div class="pro-upload-video-content">
      <!-- 已上传视频列表 -->
      <div
        class="upload-video-card"
        v-for="(item, index) in fileList"
        :key="index"
      >
        <!-- 视频缩略图/播放按钮 -->
        <div class="video-thumbnail" @click="playVideo(item.url)">
          <el-icon class="video-icon"><VideoPlay /></el-icon>
        </div>
        <!-- 视频信息 -->
        <div class="video-info" @click="playVideo(item.url)">
          <div class="video-name">{{ getFileName(item.url) }}</div>
          <div class="video-size">{{ getFileSize(item.size) }}</div>
        </div>
        <!-- 删除按钮 -->
        <el-image
          v-if="!disabled"
          src="https://static.wxb.com.cn/frontEnd/images/ideacome-vue3-component/img-close.png"
          class="video-close"
          @click="handleRemove(item, index)"
        />
      </div>
      <!-- 上传组件 -->
      <el-upload
        v-if="!disabled"
        v-loading="loading"
        ref="proUploadVideoRef"
        :class="['pro-upload-video', { 'is-disabled': disabled }]"
        v-bind="uploadProps"
        :before-upload="beforeUpload"
        :on-success="handleSuccess"
        :on-error="handleError"
        :on-exceed="handleExceed"
        :on-progress="handleProgress"
      >
        <slot>
          <div class="upload-card">
            <el-icon class="upload-icon">
              <Plus />
            </el-icon>
            <div v-if="uploadText" class="upload-text">
              {{ uploadText }}
            </div>
          </div>
        </slot>
      </el-upload>
    </div>
    <!-- 提示信息 -->
    <slot name="tip"  v-if="!disabled">
      <div class="upload-tip" v-if="tip">
        {{ tip }}
      </div>
    </slot>
  </div>
</template>

<script setup name="ProUploadVideo">
  import { ref, computed } from 'vue';
  import { Plus, VideoPlay } from '@element-plus/icons-vue';
  import { ElMessage } from 'element-plus';

  // Props 定义
  const props = defineProps({
    /** 上传地址 */
    action: {
      type: String,
      required: true,
    },
    /** 请求头 */
    headers: {
      type: Object,
      default: () => ({}),
    },
    /** 是否支持多选 */
    multiple: {
      type: Boolean,
      default: false,
    },
    /** 最大上传数量,0表示不限制 */
    limit: {
      type: Number,
      default: 0,
    },
    /** 接受的文件类型,如:.mp4,.avi,.mov */
    accept: {
      type: String,
      default: '.mp4,.avi,.mov,.wmv,.flv,.webm',
    },
    /** 文件大小限制 */
    maxSize: {
      type: Number,
      default: 0,
    },
    /** 文件大小单位(KB/MB) */
    sizeUnit: {
      type: String,
      default: 'MB',
      validator: (value) => ['KB', 'MB'].includes(value),
    },
    /** 上传提示文字 */
    uploadText: {
      type: String,
      default: '上传视频',
    },
    /** 上传提示说明 */
    tip: {
      type: String,
      default: '',
    },
    /** 是否禁用 */
    disabled: {
      type: Boolean,
      default: false,
    },
  });
  /** 初始文件列表 */
  const fileList = defineModel('fileList', {
    type: Array,
    default: () => [],
  });

  // 事件定义
  const emit = defineEmits(['success', 'error', 'exceed', 'remove', 'deleteAnnex', 'progress']);

  const proUploadVideoRef = ref();
  const loading = ref(false);

  const uploadProps = computed(() => ({
    action: props.action,
    accept: props.accept,
    limit: props.limit,
    multiple: props.multiple,
    listType: 'text',
    showFileList: false,
    headers: props.headers,
    fileList: fileList.value,
    disabled: props.disabled,
  }));

  /**
   * 获取文件名
   * @param {string} url - 文件路径
   * @returns {string} 文件名
   */
  const getFileName = (url) => {
    if (!url) return '';
    const fileName = url.substring(url.lastIndexOf('/') + 1);
    return fileName.length > 15 ? fileName.substring(0, 15) + '...' : fileName;
  };

  /**
   * 获取文件大小显示
   * @param {number} size - 文件大小(字节)
   * @returns {string} 格式化后的文件大小
   */
  const getFileSize = (size) => {
    if (!size) return '';
    const units = ['B', 'KB', 'MB', 'GB'];
    let unitIndex = 0;
    let fileSize = size;

    while (fileSize >= 1024 && unitIndex < units.length - 1) {
      fileSize /= 1024;
      unitIndex++;
    }

    return `${fileSize.toFixed(2)} ${units[unitIndex]}`;
  };

  /**
   * 上传前校验
   * @param {File} file - 待上传的文件
   * @returns {Promise<boolean>} 是否通过校验
   */
  const beforeUpload = async (file) => {
    // 校验文件类型
    const fileTypeList = props.accept
      .split(',')
      .map((item) => item.replace('.', '').toLowerCase());
    const fileType = file.name.split('.').pop().toLowerCase();

    if (!fileTypeList.includes(fileType)) {
      ElMessage({
        message: `仅支持 ${props.accept} 格式`,
        type: 'warning',
      });
      return false;
    }

    // 校验文件大小
    if (props.maxSize) {
      const fileSize = file.size;
      const maxSizeInBytes =
        props.sizeUnit === 'MB' ? props.maxSize * 1024 * 1024 : props.maxSize * 1024;
      if (fileSize > maxSizeInBytes) {
        ElMessage({
          message: `大小不能超过 ${props.maxSize}${props.sizeUnit}!`,
          type: 'warning',
        });
        return false;
      }
    }

    loading.value = true;
    return true;
  };

  /**
   * 上传进度回调
   * @param {Object} event - 进度事件对象
   * @param {Object} uploadFile - 上传文件对象
   * @param {Array} uploadFiles - 上传文件列表
   */
  const handleProgress = (event, uploadFile, uploadFiles) => {
    emit('progress', event, uploadFile, uploadFiles);
  };

  /**
   * 上传成功回调
   * @param {Object} response - 服务器响应数据
   * @param {Object} uploadFile - 上传文件对象
   * @param {Array} uploadFiles - 上传文件列表
   */
  const handleSuccess = (response, uploadFile, uploadFiles) => {
    loading.value = false;
    if (response.code === 200) {
      fileList.value.push({
        url: response.data.url,
        name: uploadFile.name,
        size: uploadFile.size
      });
    } else {
      proUploadVideoRef.value.handleRemove(uploadFile);
      ElMessage({
        message: response.msg || response.message || '上传失败',
        type: 'error',
      });
    }
    emit('success', response, uploadFile, uploadFiles);
  };

  /**
   * 上传失败回调
   * @param {Error} error - 错误信息
   * @param {Object} uploadFile - 上传文件对象
   * @param {Array} uploadFiles - 上传文件列表
   */
  const handleError = (error, uploadFile, uploadFiles) => {
    loading.value = false;
    ElMessage({
      message: '上传失败',
      type: 'error',
    });
    emit('error', error, uploadFile, uploadFiles);
  };

  /**
   * 超出限制回调
   * @param {Array} files - 超出限制的文件列表
   * @param {Array} uploadFiles - 已上传的文件列表
   */
  const handleExceed = (files, uploadFiles) => {
    ElMessage({
      message: `最多只能上传 ${props.limit} 个视频`,
      type: 'warning',
    });
    emit('exceed', files, uploadFiles);
  };

  /**
   * 移除视频
   * @param {Object} file - 要移除的文件对象
   * @param {number} index - 文件索引
   */
  const handleRemove = (file, index) => {
    fileList.value.splice(index, 1);
    proUploadVideoRef.value.handleRemove(file);
    emit('deleteAnnex', index);
  };

  /**
   * 播放视频
   * @param {string} url - 视频地址
   */
  const playVideo = (url) => {
    if (!url) return;

    // 创建视频播放弹窗
    const videoDialog = document.createElement('div');
    videoDialog.style.cssText = `
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.9);
      display: flex;
      justify-content: center;
      align-items: center;
      z-index: 9999;
    `;

    // 创建视频元素
    const videoWrapper = document.createElement('div');
    videoWrapper.style.cssText = `
      position: relative;
      max-width: 90%;
      max-height: 90%;
    `;

    // 创建加载提示
    const loadingIndicator = document.createElement('div');
    loadingIndicator.innerHTML = '视频加载中...';
    loadingIndicator.style.cssText = `
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      color: white;
      font-size: 16px;
      z-index: 1;
    `;
    videoWrapper.appendChild(loadingIndicator);

    const videoElement = document.createElement('video');
    videoElement.controls = true;
    videoElement.autoplay = true;
    videoElement.style.cssText = `
      max-width: 100%;
      max-height: 80vh;
      outline: none;
      background: black;
      display: none; /* 初始隐藏,等待加载完成后再显示 */
    `;

    // 尝试多种视频格式
    const fileExtension = url.split('.').pop().toLowerCase();
    const sourceElement = document.createElement('source');
    sourceElement.src = url;

    // 根据文件扩展名设置正确的 MIME 类型
    const mimeTypes = {
      'mp4': 'video/mp4',
      'webm': 'video/webm',
      'ogg': 'video/ogg',
      'avi': 'video/avi',
      'mov': 'video/quicktime',
      'wmv': 'video/x-ms-wmv',
      'flv': 'video/x-flv'
    };

    sourceElement.type = mimeTypes[fileExtension] || 'video/mp4';
    videoElement.appendChild(sourceElement);

    // 视频加载成功的处理
    videoElement.onloadeddata = () => {
      // 隐藏加载指示器并显示视频
      if (videoWrapper.contains(loadingIndicator)) {
        videoWrapper.removeChild(loadingIndicator);
      }
      videoElement.style.display = 'block';
    };

    // 视频加载失败的处理
    videoElement.onerror = () => {
      // 隐藏加载指示器
      if (videoWrapper.contains(loadingIndicator)) {
        videoWrapper.removeChild(loadingIndicator);
      }

      // 显示错误信息
      const errorIndicator = document.createElement('div');
      errorIndicator.innerHTML = '视频加载失败,请稍后重试';
      errorIndicator.style.cssText = `
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        color: #ff6b6b;
        font-size: 16px;
        background: rgba(0, 0, 0, 0.7);
        padding: 10px 20px;
        border-radius: 4px;
        z-index: 1;
      `;
      videoWrapper.appendChild(errorIndicator);

      // 3秒后自动关闭
      setTimeout(() => {
        if (document.body.contains(videoDialog)) {
          document.body.removeChild(videoDialog);
        }
      }, 3000);
    };

    // 创建关闭按钮
    const closeButton = document.createElement('button');
    closeButton.innerHTML = '&times;';
    closeButton.style.cssText = `
      position: absolute;
      top: -40px;
      right: 0;
      background: transparent;
      border: none;
      color: white;
      font-size: 36px;
      cursor: pointer;
      width: 40px;
      height: 40px;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: transform 0.2s;
    `;

    closeButton.onmouseover = () => {
      closeButton.style.transform = 'scale(1.1)';
    };

    closeButton.onmouseout = () => {
      closeButton.style.transform = 'scale(1)';
    };

    closeButton.onclick = () => {
      // 暂停视频并移除弹窗
      videoElement.pause();
      if (document.body.contains(videoDialog)) {
        document.body.removeChild(videoDialog);
      }
    };

    videoWrapper.appendChild(videoElement);
    videoWrapper.appendChild(closeButton);
    videoDialog.appendChild(videoWrapper);
    document.body.appendChild(videoDialog);

    // 点击背景关闭
    videoDialog.onclick = (e) => {
      if (e.target === videoDialog) {
        videoElement.pause();
        if (document.body.contains(videoDialog)) {
          document.body.removeChild(videoDialog);
        }
      }
    };

    // ESC键关闭
    const handleEscKey = (e) => {
      if (e.key === 'Escape') {
        videoElement.pause();
        if (document.body.contains(videoDialog)) {
          document.body.removeChild(videoDialog);
        }
        document.removeEventListener('keydown', handleEscKey);
      }
    };

    document.addEventListener('keydown', handleEscKey);
  };
</script>

<style lang="scss" scoped>
.pro-upload-video-box {
  .pro-upload-video-content {
    display: flex;
    // flex-wrap: wrap;
    // 已上传视频卡片样式
    .upload-video-card {
      width: 100%;
      max-width: 300px;
      height: 100px;
      position: relative;
      margin: 0 12px 12px 0;
      display: flex;
      align-items: center;
      border: 1px solid #ebeef5;
      border-radius: 6px;
      padding: 10px;
      box-sizing: border-box;

      // 视频缩略图样式
      .video-thumbnail {
        width: 50px;
        height: 50px;
        background-color: #ecf5ff;
        border-radius: 6px;
        display: flex;
        align-items: center;
        justify-content: center;
        margin-right: 10px;
        cursor: pointer;
        transition: all 0.3s;

        &:hover {
          background-color: #409eff;
          .video-icon {
            color: white;
          }
        }

        .video-icon {
          font-size: 24px;
          color: #409eff;
        }
      }

      // 视频信息样式
      .video-info {
        flex: 1;
        min-width: 0;
        cursor: pointer;

        .video-name {
          font-size: 14px;
          color: #606266;
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
          margin-bottom: 5px;
        }

        .video-size {
          font-size: 12px;
          color: #909399;
        }
      }

      // 删除按钮样式
      .video-close {
        position: absolute;
        right: -8px;
        top: -8px;
        width: 20px;
        height: 20px;
        cursor: pointer;
        z-index: 2;
      }
    }

    // 禁用状态样式
    .is-disabled {
      :deep(.el-upload--text) {
        cursor: not-allowed;
      }
    }
    // 上传按钮样式
    .pro-upload-video {
      margin-bottom: 12px;
      .upload-card {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        width: 100px;
        height: 100px;
        border: 1px dashed #d9d9d9;
        border-radius: 6px;
        cursor: pointer;
        transition: border-color 0.3s;

        &:hover {
          border-color: #409eff;
        }

        .upload-icon {
          font-size: 28px;
          color: #8c939d;
          margin-bottom: 5px;
        }

        .upload-text {
          line-height: 24px;
          color: #8c939d;
          font-size: 14px;
          text-align: center;
        }
      }
    }

    // 上传组件样式覆盖
    :deep(.el-upload) {
      width: auto;
      height: auto;
    }
  }
  // 提示文字样式
  .upload-tip {
    font-size: 12px;
    color: #909399;
  }
}
</style>
相关推荐
用户461169657284 分钟前
网站 HTTPS 不稳定?证书排查指南
前端
n876515 分钟前
Playwright Java WEB UI 自动化实现框架与逻辑
java·前端·ui
keyipatience17 分钟前
TypeScript学习(1)
前端·javascript·学习·typescript·html·css3
xifangge20251 小时前
基于 JavaScript 实现 CNC 数控切削参数与螺纹公差纯前端核算引擎
开发语言·前端·javascript
wangruofeng1 小时前
2026 Web 技术选型指南,AI 时代如何选择适合项目的技术栈
前端·aigc·ai编程
honkun61 小时前
vue 表格组件 vxe-table 修改滚动条样式,修改滚动条样式,统一宽度、颜色与自动隐藏
前端·javascript·vue.js
爱勇宝2 小时前
研究数字孪生的这半年,做了个 Demo,也有了一些新想法
前端·程序员
不可能片场2 小时前
工程实践 一个缓存把选片从十分钟压到一分钟
前端·electron
前端小万2 小时前
探索副业五个月,最终以放弃告终
前端·后端·面试
FRing032 小时前
JavaScript入门到精通二十四(正则表达式、元字符、修饰符)
开发语言·前端·javascript·正则表达式