【sgUploadTray_v2】自定义组件:升级版上传托盘自定义组件,可实时查看上传列表进度,可以通过选项卡切换上传中、成功、失败的队列,支持翻页,解决了列表内容太多导致卡顿的情况。(一)

简介: 【sgUploadTray_v2】自定义组件:升级版上传托盘自定义组件,可实时查看上传列表进度,可以通过选项卡切换上传中、成功、失败的队列,支持翻页,解决了列表内容太多导致卡顿的情况。


2024年3月20日 更新特性:支持任务管理设置同时上传的最大任务数,满足个性化同时上传数量需求。

特性:

  1. 可以全屏
  2. 可以还原尺寸、拖拽调整托盘尺寸
  3. 可以最小化
  4. 可以回到右下角默认位置
  5. 支持删除队列数据(支持一键清除所有上传记录)
  6. 支持清空已经上传成功的记录
  7. 上传过程在百分号右侧会有旋转加载动画
  8. 支持提示上传超大文件的title
  9. 实时加载上传进度条(逼真的已加载数据大小)
  10. 支持清除队列中失败的记录
  11. 支持显示实时上传速度、已耗时间、剩余上传时间
  12. 支持项卡切换上传中、成功、失败的队列
  13. 支持列表翻页
  14. 支持动态设置【同时上传的最大任务数】

sgUploadTray_v2源码

<template>
  <!-- 
新特性:
1、支持用户自定义同时上传任务数量2024.03.19
 
   -->
  <div :class="$options.name" :show="show" :size="size" :style="style">
    <div class="upload-list-tray">
      <!-- 托盘头部 -->
      <div class="header" ref="header" @dblclick.stop.prevent="dblclickHeader">
        <div class="left">
          <div class="title">
            <span class="upload-count" slot="reference">上传队列</span>
            <div class="upload-info" v-if="liveSpeed && liveSpeed > 0">
              <el-divider :direction="`vertical`" />
              <div class="info-item live-speed">
                <label>速度</label>
                <span>{{ $g.getSize(liveSpeed) }}/s</span>
              </div>
              <div class="info-item taken-time" v-if="takenTime && takenTime > 0">
                <label>已耗时</label>
                <span>{{
                  $g.date.toHourMinuteSecondByMillisecond(takenTime * 1000, {
                    zh: true, //中文单位
                    hideMilliSecond: true, //隐藏毫秒
                    hideZero: true, //隐藏为0的时间单位
                  })
                }}</span>
              </div>
              <div class="info-item remain-time" v-if="remainTime && remainTime > 0">
                <label>剩余</label
                ><span>{{
                  $g.date.toHourMinuteSecondByMillisecond(remainTime * 1000, {
                    zh: true, //中文单位
                    hideMilliSecond: true, //隐藏毫秒
                    hideZero: true, //隐藏为0的时间单位
                  })
                }}</span>
              </div>
            </div>
          </div>
        </div>
        <div class="right" @mousedown.stop>
          <!-- 控制文件的图标按钮 -->
          <div class="file-btns" v-if="showDelSuccessIconBtn || showErrorIconBtn">
            <div
              class="icon-btn"
              v-if="showDelSuccessIconBtn"
              @dblclick.stop
              @click.stop="clearAllSuccessFile"
              title="清除所有已经成功的上传记录"
            >
              <i class="el-icon-delete" style="color: #67c23a"></i>
            </div>
            <div
              class="icon-btn"
              v-if="showErrorIconBtn"
              @dblclick.stop
              @click.stop="clearAllErrorFile"
              title="清除所有失败的上传记录"
            >
              <i class="el-icon-delete-solid" style="color: #f56c6c"></i>
            </div>
            <div
              class="icon-btn"
              v-if="showErrorIconBtn"
              @dblclick.stop
              @click.stop="uploadAllErrorFile"
              title="重新上传所有失败的文件"
            >
              <i class="el-icon-upload2" style="color: #409eff"></i>
            </div>
          </div>
          <!-- 控制托盘的图标按钮 -->
          <div class="tray-btns">
            <div
              class="icon-btn"
              v-if="size !== 'lg' && showRightBottomBtn"
              @dblclick.stop
              @click.stop="toRightBottomPosition"
              title="回到原来的位置"
            >
              <i class="el-icon-bottom-right"></i>
            </div>
            <div
              class="icon-btn"
              v-if="size !== 'mn'"
              @dblclick.stop
              @click.stop="size = 'mn'"
              title="最小化"
            >
              <i class="el-icon-minus"></i>
            </div>
            <div
              class="icon-btn"
              v-if="size !== 'md'"
              @dblclick.stop
              @click.stop="size = 'md'"
              title="还原"
            >
              <i :class="size === 'lg' ? 'el-icon-copy-document' : 'el-icon-d-caret'"></i>
            </div>
            <div
              class="icon-btn"
              v-if="size !== 'lg'"
              @dblclick.stop
              @click.stop="size = 'lg'"
              title="全屏"
            >
              <i class="el-icon-full-screen"></i>
            </div>
            <div class="icon-btn" @dblclick.stop @click.stop="close">
              <i class="el-icon-close"></i>
            </div>
          </div>
        </div>
      </div>
      <div class="body">
        <div class="left" :collapse="collapseMenu">
          <el-menu
            :show-timeout="0"
            :default-active="defaultMenuActive"
            :background-color="'white'"
            :text-color="'#333'"
            :active-text-color="'#409EFF'"
            :collapse="collapseMenu"
            :unique-opened="false"
            @select="menuSelect"
          >
            <template v-for="(a, i) in menuList">
              <!-- 有子栏目--->
              <el-submenu
                :key="i"
                :index="a.path"
                v-if="a.children && a.children.length"
                :disabled="a.disabled"
              >
                <template slot="title">
                  <i :class="a.icon" v-if="a.icon" />
                  <span>{{ a.label }}</span>
                </template>
                <el-menu-item-group>
                  <el-menu-item
                    v-for="(a, i) in a.children"
                    :key="i"
                    :index="a.path"
                    :disabled="a.disabled"
                  >
                    <i :class="a.icon" v-if="a.icon" />
                    <span>{{ a.label }}</span>
                  </el-menu-item>
                </el-menu-item-group>
              </el-submenu>
              <!-- 没有子栏目-->
              <el-tooltip
                :content="a.label"
                :placement="`left`"
                :disabled="!collapseMenu"
                v-else
              >
                <el-menu-item :key="i" :index="a.path" :disabled="a.disabled">
                  <i :class="a.icon" v-if="a.icon" />
                  <span>{{ a.label }}</span>
                </el-menu-item>
              </el-tooltip>
            </template>
          </el-menu>
          <!-- 折叠按钮 -->
          <div class="collapseBtn" @click="collapseMenu = !collapseMenu">
            <i class="el-icon-caret-right" v-if="collapseMenu" />
            <i class="el-icon-caret-left" v-else />
          </div>
        </div>
 
        <!-- 设置 ------------------------------------------>
        <div class="right" v-if="defaultMenuActive === `set`">
          <div class="set-list">
            <el-collapse v-model="collapseActiveName_set" accordion>
              <el-collapse-item
                :name="parseInt(index)"
                v-for="(item, index) in collapseItems_set"
                :key="index"
              >
                <template slot="title">
                  <h1>{{ item.title }}</h1>
                </template>
                <div class="form-body">
                  <el-form @submit.native.prevent label-position="right" size="mini">
                    <el-form-item :label="`同时上传的最大任务数`" label-width="">
                      <el-input-number
                        style="width: 100px"
                        v-model.trim="setData.uploadMaxCount"
                        :precision="0"
                        :step="1"
                        :min="1"
                        :max="10"
                        :controls-position="`left`"
                      />
                    </el-form-item>
                  </el-form>
                </div>
              </el-collapse-item>
            </el-collapse>
          </div>
        </div>
 
        <!-- 上传中的文件列表 ------------------------------------------>
        <div class="right" v-else>
          <div class="upload-file-list">
            <ul v-if="tableData.length">
              <li
                v-for="(a, i) in tableData"
                :key="i"
                :title="
                  a.size > 1024 * 1024 * 500
                    ? `超大文件上传中,请耐心等待,切勿关闭或刷新浏览器!`
                    : ''
                "
              >
                <div class="left">
                  <div class="icon-btns">
                    <el-button
                      title="移出上传队列"
                      :show="a.status === 'error'"
                      class="remove-icon-btn icon-btn"
                      type="danger"
                      icon="el-icon-delete-solid"
                      size="mini"
                      plain
                      circle
                      @click.stop="removeUploadFile(a)"
                    ></el-button>
                  </div>
                  <!-- 动画加载旋转 -->
                  <div
                    class="fileLoading"
                    v-loading="a.percent <= 100"
                    v-if="a.status !== 'error' && a.status !== 'success'"
                  ></div>
                  <!-- 上传成功icon -->
                  <div class="loadingSuccessIcon" v-if="a.status === `success`">
                    <i class="el-icon-success" style="color: #67c23a"></i>
                  </div>
                  <!-- 上传失败icon -->
                  <div class="loadingEorrorIcon" v-if="a.status === 'error'">
                    <i class="el-icon-error" style="color: #f56c6c"></i>
                  </div>
                  <span class="name" :title="a.filePath || a.name">
                    {{ a.filePath || a.name }}
                    <!-- {{ a.filePath && a.filePath.includes(`/`) ? `[路径:${a.filePath}]` : "" }} -->
                  </span>
                  <el-tag class="size" size="mini"
                    >{{ $g.getSize(a.size * (a.percent / 100)) }}/{{
                      $g.getSize(a.size)
                    }}</el-tag
                  >
                  <!-- <el-progress class="progress" :percentage="a.percent"></el-progress> -->
                  <el-progress
                    class="progress"
                    style="width: 100%"
                    type="line"
                    :percentage="parseInt(a.percent)"
                    :show-text="true"
                    :stroke-width="10"
                    :text-inside="false"
                    :color="'#409EFF'"
                    :define-back-color="'#eee'"
                  />
                </div>
                <div class="right">
                  <span class="tip" :color="a.color">{{ a.tip }}</span>
                  <div
                    class="icon-btns"
                    v-if="a.status !== `uploading` && a.status !== `success`"
                  >
                    <el-button
                      title="重新上传"
                      class="upload-icon-btn icon-btn"
                      type="primary"
                      icon="el-icon-upload2"
                      size="mini"
                      plain
                      circle
                      @click.stop="startUploadFile(a)"
                    ></el-button>
                  </div>
                </div>
              </li>
            </ul>
            <!-- 自定义空状态 -->
            <el-empty v-else>
              <div slot="image"><img :src="require('@/assets/404.png')" /></div>
              <div slot="description">{{ getEmptyText() }}</div>
            </el-empty>
          </div>
          <el-pagination
            style="width: 100%; text-align: center; margin-top: 10px"
            background
            :hidden="total <= 10"
            :layout="`total, sizes, prev, pager, next, jumper`"
            :page-sizes="[10, 20, 50, 100]"
            :pager-count="7"
            :current-page.sync="currentPage"
            :page-size.sync="pageSize"
            :total="total"
            @size-change="initList"
            @current-change="initList"
          />
        </div>
      </div>
      <div class="footer">
        <div class="text" v-html="popoverContent"></div>
        <div class="progress" v-if="uploadList.length > 1 && totalPercentage < 100">
          <label>。总进度</label>
          <el-progress
            style="width: 100%"
            type="line"
            :percentage="parseInt(totalPercentage)"
            :show-text="true"
            :stroke-width="10"
            :text-inside="false"
            :color="'#409EFF'"
            :define-back-color="'#eee'"
          />
        </div>
      </div>
    </div>
 
    <!-- 拖拽移动窗体 -->
    <sgDragMove
      :data="dragMoveDoms"
      :cursor="{
        grab: 'default',
        grabbing: 'default',
      }"
      nearPadding="10"
      :disabled="size === 'lg' && disabledDragMove"
      @dragStart="$emit(`dragStart`, dragMoveDoms)"
      @dragging="
        showRightBottomBtn = true;
        $emit(`dragging`, dragMoveDoms);
      "
      @dragEnd="$emit(`dragEnd`, dragMoveDoms)"
      mousemoveNearSide
    />
    <!-- 拖拽改变窗体尺寸 -->
    <sgDragSize
      v-if="resizeable_"
      :disabled="size === 'lg'"
      @dragStart="disabledDragMove = true"
      @dragging="draggingSize"
      @dragEnd="disabledDragMove = false"
      :minWidth="minWidth"
      :minHeight="minHeight"
    />
  </div>
</template>
<script>
import $g from "@/js/sg"; //必须要单独引入,否者用$g.mount挂在该组件的时候无法获取到$g
import sgDragMove from "@/vue/components/admin/sgDragMove";
import sgDragSize from "@/vue/components/admin/sgDragSize";
export default {
  name: "sgUploadTray_v2",
  components: {
    sgDragMove,
    sgDragSize,
  },
  data() {
    return {
      currentPage: 1,
      pageSize: 10,
      total: 0,
      tableData: [], //当前显示队列(不代表所有的上传队列,只为不要太卡)
      // maxShowUploadFileCount: 10, //默认展示上传数量
      // expandAllUploadList: false, //默认折叠
      minWidth: 950,
      minHeight: 40,
      style_bk: null,
      style: {},
      resizeable_: true,
      disabledDragMove: false, //屏蔽移动
      show: false,
      showRightBottomBtn: false,
      size: "md", //lg全屏、md普通、mn最小
      uploadList: [],
      uploadingFiles: [], //真正上传中的文件对象数组
      dragMoveDoms: [
        /* {
          canDragDom: elementDOM,//可以拖拽的位置元素
          moveDom: elementDOM,//拖拽同步移动的元素
      } */
      ], //可以拖拽移动的物体
      lastUploadedTotalSize: 0, //记录上次已经下载完成的总大小
      liveSpeed: 0, //瞬时下载速度(单位B)
      takenTime: 0, //已耗时
      remainTime: 0, //剩余下载时长
      interval: null,
      second: 1, //轮训间隔秒钟
      successFileList: [], //成功文件列表
      errorFileList: [], //失败文件列表
      remainFileList: [], //剩余文件列表
      collapseActiveName_set: 0,
      collapseItems_set: [
        // { value: 1, title: "基本设置", content: "开发中" },
        { value: 2, title: "任务管理", content: "开发中" },
        // { value: 3, title: "上传设置", content: "开发中" },
        // { value: 4, title: "提醒", content: "开发中" },
        // { value: 5, title: "高级设置", content: "开发中" },
      ],
      // defaultMenuActive: `uploading`, //当前激活菜单的 index
      defaultMenuActive: `set`, //测试
      collapseMenu: false, //是否水平折叠收起菜单(仅在 mode 为 vertical 时可用)
      menuList: [
        {
          label: "上传中",
          path: "uploading",
          icon: "el-icon-upload2",
        },
        {
          label: "已完成",
          path: "success",
          icon: "el-icon-success",
        },
        {
          label: "失败",
          path: "error",
          icon: "el-icon-error",
        },
        {
          label: "设置",
          path: "set",
          icon: "el-icon-s-tools",
        },
      ],
      setData: {
        uploadMaxCount: 5, //同时上传的最大任务数
      }, //上传设置配置参数
    };
  },
  props: ["data", "value", "resizeable", "position", "store"],
  watch: {
    setData: {
      handler(d) {
        if (d && Object.keys(d).length) {
          // console.log(this.store, this.$store);
          this.store && (this.store.getters._global.uploadSetData = d); //用于$g.mount挂在该组件的时候无法获取到$store
          this.$store && (this.$store.getters._global.uploadSetData = d);
        }
      },
      deep: true,
      immediate: true,
    },
    value: {
      handler(d) {
        this.show = d;
      },
      deep: true,
      immediate: true,
    },
    show: {
      handler(d) {
        d && (this.defaultMenuActive = `uploading`);
        this.$emit(`input`, d);
      },
      deep: true,
      immediate: true,
    },
    data: {
      handler(d) {
        this.uploadList = d || [];
      },
      deep: true,
      immediate: true,
    },
    uploadList: {
      handler(newValue, oldValue) {
        if (newValue && Object.keys(newValue).length) {
          this.interval || this.startUploadCalcLiveSpeed();
          this.successFileList = newValue.filter((v) => v.status === `success`); //成功队列
          this.errorFileList = newValue.filter((v) => v.status === "error"); //失败队列
          this.remainFileList = newValue.filter(
            (v) => v.status !== "error" && v.status !== "success"
          ); //还需要上传的队列
        } else {
          this.successFileList = [];
          this.errorFileList = [];
          this.remainFileList = [];
        }
        // 计算正上传中的文件对象数组----------------------------------------
        this.uploadingFiles = (newValue || []).filter((v) => v.status === "uploading");
        this.$emit(`changeUploadingListClose`, {
          path: this.position,
          close: this.close,
        });
        // ----------------------------------------
        this.initList();
      },
      deep: true, //深度监听
      immediate: true, //立即执行
    },
    resizeable: {
      handler(newValue, oldValue) {
        this.resizeable_ = newValue === "" || newValue;
      },
      deep: true, //深度监听
      immediate: true, //立即执行
    },
    size: {
      handler(newValue, oldValue) {
        switch (newValue) {
          case "lg":
          case "mn":
            this.style_bk = JSON.parse(JSON.stringify(this.style));
            delete this.style.width, delete this.style.height;
            break;
          case "md":
            this.style_bk && (this.style = JSON.parse(JSON.stringify(this.style_bk)));
            break;
        }
      },
      deep: true, //深度监听
      immediate: true, //立即执行
    },
  },
  computed: {
    showDelSuccessIconBtn(d) {
      return this.uploadList.some((v) => v.status === `success`);
    },
    showErrorIconBtn(d) {
      return this.uploadList.some((v) => v.status === "error");
    },
    popoverContent(d) {
      let r = [];
      this.successFileList.length &&
        r.push(
          `已上传成功<span style="color: #67C23A;">${this.successFileList.length}</span>个`
        );
      this.errorFileList.length &&
        r.push(`失败<span style="color: #F56C6C;">${this.errorFileList.length}</span>个`);
      this.remainFileList.length &&
        r.push(
          `剩余<span style="color: #409EFF;">${this.remainFileList.length}</span>个`
        );
      if (this.uploadList.length) {
        return `共计${this.uploadList.length}个文件,${
          r.length ? `${r.join(",")}文件` : ``
        }`;
      } else {
        return `暂无待上传文件`;
      }
    },
    // 总体进度
    totalPercentage() {
      if (this.uploadList.length) {
        return parseFloat(
          ((this.successFileList.length / this.uploadList.length) * 100).toFixed(2)
        );
      } else {
        return 0;
      }
    },
  },
  mounted() {
    this.$el.style.setProperty("--minWidth", `${this.minWidth}px`); //js往css传递局部参数
    this.$el.style.setProperty("--minHeight", `${this.minHeight}px`); //js往css传递局部参数
    this.dragMoveDoms = [
      {
        canDragDom: this.$refs.header, //托盘的头部可以拖拽
        moveDom: this.$el, //拖拽的时候,整个上传列表一起跟随移动
      },
    ];
  },
  destroyed() {
    clearInterval(this.interval);
  },
  methods: {
    saveSet(d) {},
    getEmptyText() {
      let label = (this.menuList.find((v) => v.path === this.defaultMenuActive) || {})
        .label;
      return `暂无${label ? `${label}的` : ``}文件`;
    },
    //菜单激活回调
    menuSelect(index, path) {
      this.defaultMenuActive = index;
      //做其他操作
      this.initList();
    },
    //静态数据翻页(支持筛选搜索)
    initList({ keyword = this.keyword } = {}) {
      let results = this.uploadList.filter((v) =>
        keyword ? v.name.includes(keyword) : true
      );
      switch (this.defaultMenuActive) {
        case "uploading":
          results = results.filter(
            (v, i, ar) => v.status === `uploading` || v.status === ``
          );
          break;
        case "success":
        case "error":
          results = results.filter((v, i, ar) => v.status === this.defaultMenuActive);
          break;
        default:
      }
      this.total = results.length;
      this.tableData = results.slice(
        (this.currentPage - 1) * this.pageSize,
        this.currentPage * this.pageSize
      );
    },
    // 开始计算瞬时下载速度
    startUploadCalcLiveSpeed() {
      clearInterval(this.interval);
      this.interval = setInterval(() => {
        this.calcLiveSpeed();
      }, 1000 * this.second);
    },
    // 结束计算瞬时下载速度
    endUploadCalcLiveSpeed(d) {
      clearInterval(this.interval);
      this.interval = null;
      this.liveSpeed = 0;
      this.takenTime = 0;
      this.remainTime = 0;
    },
    // 没有上传进程文件才结束计算速度
    ifNoUploadingFile_EndCalcLiveSpeed(d) {
      this.uploadingFiles.length || this.endUploadCalcLiveSpeed();
    },
    // 计算瞬时下载速度
    calcLiveSpeed(d) {
      this.takenTime++;
      let uploadList = this.uploadList;
      if (uploadList.length) {
        let totalSize = uploadList.reduce(
          (prevResult, current) => prevResult + current.size,
          0
        ); //求和需要上传的文件总大小
        let uploadedTotalSize = uploadList.reduce(
          (prevResult, current) => prevResult + current.size * (0.01 * current.percent),
          0
        ); //求和已经上传的文件总大小
        let remainTotalSize = totalSize - uploadedTotalSize; //剩余需要上传的文件
        if (this.lastUploadedTotalSize) {
          this.liveSpeed = (uploadedTotalSize - this.lastUploadedTotalSize) / this.second; //瞬时速度
          this.remainTime = remainTotalSize / this.liveSpeed; //瞬时剩余时长
        } else {
          this.liveSpeed = 0;
        }
        this.lastUploadedTotalSize = uploadedTotalSize; //记录本次已经上传的总大小
      } else {
        this.endUploadCalcLiveSpeed();
      }
    },
    clearAllSuccessFile() {
      let successFileList = this.uploadList.filter((v) => v.status === `success`);
      if (successFileList.length === 0)
        return this.$message(`暂无可以移除的成功记录,请稍后再试!`);
      this.$emit(`clearAllSuccessFile`, successFileList);
      // this.$nextTick(() => {
      successFileList.forEach((file) => file.removeFile()); //移除原始队列&托盘队列
      this.uploadList = this.uploadList.filter((v) => v.percent <= 100);
      // });
    },
    clearAllErrorFile() {
      let errorFileList = this.uploadList.filter((v) => v.status === "error");
      if (errorFileList.length === 0)
        return this.$message(`暂无可以移除的失败记录,请稍后再试!`);
      this.$emit(`clearAllErrorFile`, errorFileList);
      this.$nextTick(() => {
        errorFileList.forEach((file) => file.removeFile()); //移除原始队列&托盘队列
        this.uploadList = this.uploadList.filter((v) => v.status !== "error");
      });
    },
    uploadAllErrorFile(d) {
      let errorFileList = this.uploadList.filter((v) => v.status === "error");
      errorFileList.forEach((fileData) => this.startUploadFile(fileData));
      if (errorFileList.length === 0) return this.$message(`暂无失败记录,请稍后再试!`);
      this.$emit(`uploadAllErrorFile`, errorFileList);
    },
    draggingSize({ style }) {
      this.disabledDragMove = true;
      this.style = style;
    },
    toRightBottomPosition(d) {
      this.showRightBottomBtn = false;
      let rect = this.$el.getBoundingClientRect();
      this.$el.style.left = `${innerWidth - rect.width}px`;
      this.$el.style.top = `${innerHeight - rect.height}px`;
      // 用下面的写法会清除掉setProperty属性
      /* this.$el.style = {
        left: innerWidth - rect.width + "px",
        top: innerHeight - rect.height + "px",
      }; */
    },
    dblclickHeader(d) {
      switch (this.size) {
        case "lg":
          this.size = "md";
          break;
        case "md":
          this.size = "mn";
          break;
        case "mn":
          this.size = "md";
          break;
        default:
          F;
      }
    },
    removeAllFilesFromList() {
      this.uploadList &&
        this.uploadList.length &&
        this.uploadList.slice(-1)[0].removeAllFile(); //移除原始队列&托盘队列
    },
    removeFileFromList(d) {
      d.removeFile(); //移除原始队列&托盘队列
    },
    // 重新上传失败的记录
    startUploadFile(fileData) {
      fileData.startUpload({ handleTrigger: true });
    },
    // 移出某一个队列文件
    removeUploadFile(d) {
      if (d.status === "uploading") {
        this.$confirm(`${d.name}正在上传中,确定要取消吗?`, `提示`, {
          dangerouslyUseHTMLString: true,
          confirmButtonText: `确定`,
          cancelButtonText: `取消`,
          type: "warning",
        })
          .then(() => {
            this.$emit(`stopUpload`, [d]);
            this.$nextTick(() => {
              this.removeFileFromList(d);
              this.ifNoUploadingFile_EndCalcLiveSpeed();
            });
          })
          .catch(() => {});
      } else {
        this.removeFileFromList(d);
      }
    },
    // 关闭托盘
    close({ cb } = {}) {
      let stopUploadList = this.uploadingFiles;
      if (stopUploadList.length) {
        this.$confirm(`您还有正在上传中的文件,确定要取消吗?`, `提示`, {
          dangerouslyUseHTMLString: true,
          confirmButtonText: `确定`,
          cancelButtonText: `取消`,
          type: "warning",
        })
          .then(() => {
            this.doRemove({ stopUploadList, cb });
          })
          .catch(() => {});
      } else {
        this.doRemove({ stopUploadList, cb });
      }
    },
    doRemove({ stopUploadList, cb } = {}) {
      this.show = false;
      this.$emit(`stopUpload`, stopUploadList);
      this.$nextTick(() => {
        this.endUploadCalcLiveSpeed();
        this.removeAllFilesFromList(); //清空上传列表
        cb && cb(stopUploadList); //完成后回调
      });
    },
  },
};
</script>
<style lang="scss" scoped>
.sgUploadTray_v2 {
  $bodyLeftWidth: 150px; //左侧分类菜单宽度
  $headerHeight: 40px; //头部高度
  $footerHeight: 40px; //底部统计上传进度高度
  $minWidth: var(--minWidth); //托盘最小宽度
  $minHeight: var(--minHeight); //托盘最小高度
  $leftIconBtnWidth: 50px; //左侧删除按钮宽度
  $rightIconBtnWidth: 50px; //右侧重新上传按钮宽度
  $loadingWidth: 30px; //加载动画宽度(旋转全全)
  $rightWidth: 150px; //右侧宽度
  $sizeWidth: 200px; //文件大小宽度宽度
  $progressWidth: 100px; //进度条宽度
  $tipWidth: 100px; //提示文本宽度
  // ----------------------------------------
  z-index: 2001; //根据情况自己拿捏(太大了会遮住element的其他弹窗组件),v-loading默认是2000的z-index
  user-select: none;
  position: fixed;
  right: 10px;
  bottom: 10px;
  width: $minWidth;
  background-color: white;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid #eee;
  font-size: 14px;
  display: none;
  &[show] {
    display: block;
  }
  &[size="lg"] {
    left: 0 !important;
    top: 0 !important;
    width: 100vw;
    height: 100vh;
    transition: none;
    .upload-file-list {
      max-height: calc(100vh - 60px) !important;
    }
  }
  &[size="md"] {
    width: $minWidth;
    height: revert;
  }
  &[size="mn"] {
    width: $minWidth;
    height: $minHeight;
  }
  .upload-list-tray {
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    padding-bottom: 20px;
    width: 100%;
    height: 100%;
    position: relative;
    .header {
      flex-shrink: 0;
      font-size: 16px;
      font-weight: bold;
      width: 100%;
      height: $headerHeight;
      box-sizing: border-box;
      padding: 10px 20px;
      /*从上往下线性渐变背景*/
      background: linear-gradient(#409eff11, white);
      color: #409eff;
      display: flex;
      justify-content: space-between;
      align-items: center;
      .left {
        display: flex;
        align-items: center;
        flex-grow: 1;
        .title {
          display: flex;
          align-items: center;
          flex-wrap: nowrap;
          .upload-info {
            display: flex;
            align-items: center;
            color: black;
            flex-shrink: 0;
            align-items: center;
            font-weight: normal;
            .info-item {
              margin-right: 5px;
              &:last-of-type {
                margin-right: 0;
              }
              span {
                font-family: DIN-Light;
                color: #409eff;
              }
              &.live-speed {
                span {
                  font-family: DIN-Black;
                }
              }
            }
          }
        }
        .icon-btns {
          display: flex;
          align-items: center;
          flex-wrap: nowrap;
          .icon-btn {
            cursor: pointer;
            margin-right: 5px;
            &:last-of-type {
              margin-right: 0;
            }
            i {
              pointer-events: none;
            }
            &:hover {
              opacity: 0.618;
            }
          }
        }
      }
      .right {
        display: flex;
        align-items: center;
        justify-content: flex-end;
        flex-shrink: 0;
        pointer-events: auto;
        .icon-btn {
          margin-left: 10px;
          cursor: pointer;
          i {
            pointer-events: none;
          }
          &:hover {
            opacity: 0.618;
          }
          &:first-of-type {
            margin-left: 0;
          }
        }
        .file-btns {
          margin-left: 10px;
          display: flex;
          flex-wrap: nowrap;
          justify-content: flex-end;
          box-sizing: border-box;
          padding: 0 10px;
          border-right: 1px solid #eee;
        }
        .tray-btns {
          margin-left: 10px;
          display: flex;
          flex-wrap: nowrap;
          justify-content: flex-end;
        }
      }
    }
    .body {
      display: flex;
      flex-wrap: nowrap;
      width: 100%;
      height: calc(100% - #{$headerHeight} - #{$footerHeight} + 20px);
      max-height: calc(100vh - #{$headerHeight} - #{$footerHeight} - 40px);
      & > .left {
        // transition: 0.382s;
        position: relative;
        flex-shrink: 0;
        width: $bodyLeftWidth;
        box-sizing: border-box;
        padding: 0 10px 0 20px;
        border-right: solid 1px #eff0f1;
        >>> .el-menu {
          transition: none;
          .el-menu-item {
            transition: none;
            margin-bottom: 5px;
            border-radius: 8px;
            &:last-of-type {
              margin-bottom: 0;
            }
            &:focus,
            &:hover {
              background-color: #f5f6f7 !important;
            }
            &.is-active {
              background-color: #e9effb !important;
            }
          }
        }
        .collapseBtn {
          transform: translateY(50%); //防止托盘最小高度的时候还冒出一小截
          width: 10px;
          height: 20px;
          display: flex;
          justify-content: center;
          align-items: center;
          color: white;
          background-color: #409eff;
          font-size: 12px;
          position: absolute;
          margin: auto;
          top: 0;
          right: -10px;
          bottom: 0;
          z-index: 1;
          border-radius: 0 4px 4px 0;
          box-sizing: border-box;
          padding: 20px 0;
          cursor: pointer;
          &:hover {
            filter: brightness(1.1);
          }
        }
        &[collapse] {
          width: 70px;
          >>> .el-menu {
            .el-menu-item {
              width: 40px;
              height: 40px;
              display: flex;
              justify-content: center;
              align-items: center;
              span {
                display: none;
              }
            }
          }
        }
      }
      & > .right {
        width: calc(100% - #{$bodyLeftWidth});
        flex-grow: 1;
        box-sizing: border-box;
        padding: 0 20px 0 10px;
        .upload-file-list {
          width: 100%;
          flex-grow: 1;
          overflow-y: auto;
          position: relative;
          max-height: calc(100% - 42px);
          min-height: 200px;
          height: 100%;
          ul {
            width: 100%;
            & > li {
              line-height: 1.6;
              box-sizing: border-box;
              padding: 10px;
              border-radius: 8px;
              display: flex;
              justify-content: space-between;
              align-items: center;
              width: 100%;
              height: 50px;
              & > .left {
                width: calc(100% - #{$rightWidth});
                display: flex;
                align-items: center;
                flex-grow: 1;
                flex-shrink: 0;
                // 上传队列记录左侧侧操作按钮
                .icon-btns {
                  display: flex;
                  flex-wrap: nowrap;
                  .icon-btn {
                    display: none;
                    &[show] {
                      margin-right: 15px;
                      display: block;
                    }
                  }
                }
                .fileLoading {
                  flex-shrink: 0;
                  width: 30px;
                  margin-right: 5px;
                  height: 0;
                  transform: scale(0.5);
                }
                .loadingSuccessIcon,
                .loadingEorrorIcon {
                  margin-right: 5px;
                  width: 30px;
                  height: 30px;
                  display: flex;
                  justify-content: center;
                  align-items: center;
                  flex-shrink: 0;
                }
                .name {
                  text-align: left;
                  margin-right: 10px;
                  width: calc(
                    100% - #{$loadingWidth} - #{$sizeWidth} - #{$progressWidth} - #{$rightWidth} -
                      20px
                  );
                  overflow: hidden;
                  white-space: nowrap;
                  text-overflow: ellipsis;
                  flex-shrink: 0;
                  flex-grow: 1;
                }
                .size {
                  margin-right: 10px;
                  max-width: $sizeWidth;
                  /*单行省略号*/
                  overflow: hidden;
                  white-space: nowrap;
                  text-overflow: ellipsis;
                  flex-shrink: 0;
                }
                >>> .progress {
                  max-width: $progressWidth;
                  display: flex;
                  align-items: center;
                  flex-wrap: nowrap;
                  flex-shrink: 0;
                  .el-progress-bar__inner {
                    transition: none;
                  }
                }
              }
              & > .right {
                display: flex;
                align-items: center;
                justify-content: flex-end;
                width: $rightWidth;
                .tip {
                  width: $tipWidth;
                  overflow: hidden;
                  white-space: nowrap;
                  text-overflow: ellipsis;
                  flex-shrink: 0;
                  text-align: right;
                  &[color="red"] {
                    color: #f56c6c;
                  }
                  &[color="green"] {
                    color: #67c23a;
                  }
                  &[color="blue"] {
                    color: #409eff;
                  }
                }
                // 上传队列记录右侧操作按钮
                .icon-btns {
                  display: flex;
                  flex-wrap: nowrap;
                  .icon-btn {
                    display: none;
                    &[show] {
                      margin-left: 15px;
                      display: block;
                    }
                  }
                }
              }
              &:hover {
                background-color: #409eff11;
                color: #409eff;
                .left {
                  // 移入上传队列记录左侧操作按钮
                  .icon-btns {
                    .icon-btn {
                      display: block;
                      &:last-of-type {
                        margin-right: 10px;
                      }
                    }
                  }
                  /* .name {
                    width: calc(
                      100% - #{$leftIconBtnWidth} - #{$loadingWidth} - #{$sizeWidth} - #{$progressWidth} -
                        #{$rightWidth} - #{$rightIconBtnWidth} - 20px
                    );
                  } */
                }
                .right {
                  .tip {
                    // margin-right: 15px;
                  }
                  // 移入上传队列记录右侧操作按钮
                  .icon-btns {
                    margin-left: 15px;
                    .icon-btn {
                      display: block;
                      &:first-of-type {
                        margin-left: 0;
                      }
                    }
                  }
                }
              }
            }
          }
          .el-empty {
            width: max-content;
            height: max-content;
            position: absolute;
            margin: auto;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
          }
        }
      }
    }
    .footer {
      z-index: 1;
      font-weight: normal;
      flex-shrink: 0;
      font-size: 14px;
      font-weight: bold;
      width: 100%;
      height: $footerHeight;
      box-sizing: border-box;
      padding: 10px 20px;
      margin-bottom: -20px;
      background: linear-gradient(white, #eff2f7);
      color: #909399;
      display: flex;
      align-items: center;
      flex-wrap: nowrap;
      white-space: nowrap;
      * {
        font-weight: normal;
      }
      .text {
        white-space: nowrap;
      }
      .progress {
        max-width: 200px;
        flex-grow: 1;
        white-space: nowrap;
        display: flex;
        align-items: center;
        flex-wrap: nowrap;
        label {
          white-space: nowrap;
          flex-shrink: 0;
          margin-right: 5px;
        }
        >>> .el-progress {
          white-space: nowrap;
          .el-progress__text {
            font-weight: normal;
            font-size: 14px !important;
          }
        }
      }
    }
  }
}
</style>

应用

<template>
  <div :class="$options.name">
    <el-button type="primary" @click="$refs.sgUpload_v2.triggerUploadFolder()"
      >点击上传文件夹</el-button
    >
 
    <!-- 上传组件 -->
    <sgUpload_v2
      :noCheckFile="true"
      ref="sgUpload_v2"
      :data="uploadData"
      :sgUploadTray="sgUploadTray"
      hideMessageSuccessTip
    />
 
    <!-- 上传托盘(右下角) -->
    <sgUploadTray ref="sgUploadTray" resizeable />
  </div>
</template>
 
<script>
import sgUpload_v2 from "@/vue/components/admin/sgUpload_v2";
import sgUploadTray from "@/vue/components/admin/sgUploadTray_v2";
 
export default {
  name: `demoUploadTray`,
  components: {
    sgUpload_v2,
    sgUploadTray,
  },
  data() {
    return {
      //上传相关变量----------------------------------------
      sgUploadTray: null,
      uploadData: {
        limit: 10000, //限制上传文件个数
        name: `FILE`,
        accept: `*`,
        actionURL: `${this.$d.API_ROOT_URL}/xxx/xxx`,
        actionData: {
          ZYGS: this.$g.getZYGS({
            BMID: this.$global.getBMID(),
            type: this.type,
          }),
          sgLog: `前端请求来源:${this.$options.name}资源上传`,
        },
      },
      // ----------------------------------------
    };
  },
  mounted(d) {
    this.sgUploadTray = this.$refs.sgUploadTray;
  },
};
</script>

基于sgUploadTray 1.0版本迭代

image.png


相关文章
针对FastAdmin新增上传多个图片,新增上传的视频的预览效果
针对FastAdmin新增上传多个图片,新增上传的视频的预览效果
919 0
element用户上传头像组件带大图预览,和删除功能
element用户上传头像组件带大图预览,和删除功能
|
7月前
|
API
【sgUpload_v2】自定义组件:升级自定义上传组件,支持拖拽上传、弹窗上传单个或多个文件/文件夹,并且获取文件夹结构路径树,然后在右下角出现上传托盘。
【sgUpload_v2】自定义组件:升级自定义上传组件,支持拖拽上传、弹窗上传单个或多个文件/文件夹,并且获取文件夹结构路径树,然后在右下角出现上传托盘。
|
7月前
|
Shell 开发工具 git
聊天功能演示系统发布后出现有些页面滚动与鼠标点击问题解决
聊天功能演示系统发布后出现有些页面滚动与鼠标点击问题解决
45 0
|
7月前
|
JavaScript
点击导出所选数据(原生js)
点击导出所选数据(原生js)
45 0
|
7月前
【sgUploadTray】自定义组件:上传托盘自定义组件,可实时查看上传列表进度。
【sgUploadTray】自定义组件:上传托盘自定义组件,可实时查看上传列表进度。
|
小程序 前端开发 JavaScript
微信小程序分类菜单激活状态跟随列表滚动自动切换
微信小程序分类菜单激活状态跟随列表滚动自动切换
173 0
微信小程序分类菜单激活状态跟随列表滚动自动切换
element ui 上传图片之后跳转、刷新、保存,预览和删除丢失问题
这问题困惑了我好久,在官方的element ui 的组件库中,直接拿来使用的话,只有当前显示效果,一旦刷新页面或者保存之后,就会丢失,预览和删除功能。当保存后,保存到后端接口,再次查看,图片是能渲染出来,但是由于保存页面刷新,随之整个上传过程失败,而查看所拿到的图片只是一张静态图片,要想再次预览和查看,需要重新选中上传
261 0
|
JavaScript
fastadmin 自定义 按钮 动态切换数据 TAB切换
fastadmin 自定义 按钮 动态切换数据 TAB切换
294 0
|
移动开发 HTML5
知道事件捕获! 🤪 但不会用来实现批量拖拽上传?(二)
知道事件捕获! 🤪 但不会用来实现批量拖拽上传?
135 0