Skip to content

用vue3 写了个上传组件列表,支持取消上传,展示进度条

最近写了个上传附件组件,回想起来之前单位有个功能模块,也是差不多文件上传功能(支持取消、支持删除、支持显示进度条),然后被同事写的是乱七八糟。维护起来非常费劲,简直就是我的眼中钉,肉中刺,一直想把这段代码给拔掉。这次刚好遇到了相似的功能,咱们就从组件设计以及实现一起来看看,怎么实现一个文件上传附件的功能!

示意图

    1. 支持文件上传 (这个肯定要支持某些文件根据自己的业务需求限制)
    1. 支持文件列表删除
    1. 支持显示附件上传进度条
    1. 支持上传中的附件取消

组件设计

咱们先来划分组件设计,以及职责划分。如下图咱们共划分为3个模块:交互层、操作层、数据层。

模块划分示意图:

示意图

模块交互示意图:

示意图

其实这么划分组件已经能很清晰看明白每个组件要做什么了!那么咱们的文件目录划分如下:

bash
├── index.vue
├── list.vue
├── operator.vue
├── options.js
└── running
    ├── index.vue
    ├── item.vue
    └── progress.vue
  • index.vue负责载各个组件
  • list.vue 负责展示上传的文件列表
  • operator.vue 负责展示操作按钮(上传、删除)
  • options.js 就放了个uuidconvertFileSize转换大小的函数
  • running 文件夹中主要负责上传文件,取消文件,展示进度条

相关实现

咱们就来分析下具体实现细节:

  1. 显示进度条以及支持取消相关功能具体如下:

进度条以及支持取消 running下item.vue代码

js
import { ref } from "vue";
import axios from "axios";

const percent = ref(0); // 记录百分比
const CancelToken = axios.CancelToken; // 取消上传
const axiosCancel = ref(null);

const uploadFile = () => {
  const formData = new FormData();
  formData.append("file", props.file);

  axios
    .post(`你自己的API`, formData, {
      headers: {
        "Content-Type": "multipart/form-data",
      },
      cancelToken: new CancelToken(function executor(c) {
        axiosCancel.value = c;
      }),
      onUploadProgress: (progressEvent) => {
        // 这里能计算出来上传的百分比
        if (progressEvent.lengthComputable) {
          percent.value = Math.round(
            (progressEvent.loaded / progressEvent.total) * 100,
          );
        }
      },
    })
    .then(() => {
      percent.value = 100;
      setTimeout(() => {
        // 完成
        emits("success", props.uuid);
      }, 300);
    })
    .catch((error) => {
      if (axios.isCancel(error)) {
        console.log("Request canceled", error.message);
      } else {
        console.log(error);
      }
    });
};

const oncancel = () => {
  axiosCancel.value?.();
  isCancel.value = true;

  setTimeout(() => {
    // 取消
    emits("cancel", props.uuid);
  }, 300);
};
  1. 上传中数据删除、新增逻辑 runningUploadFiles
  • 上传完成后删除
  • 取消后删除
  • 新选文件后新增

新增 index.vue代码

vue
<template>
  <div class="work-flow-attachment">
    <WorkFlowAttachmentRunning
      v-model="runningUploadFiles"
      :formId="formId"
      @complate="onComplate"
    />
    <WorkFlowAttachmentOperator
      :uploadFiles="uploadFiles"
      :deleteFiles="deleteFiles"
    />
    <WorkFlowAttachmentList
      ref="listRef"
      :formId="formId"
      v-model="checkData"
    />
  </div>
</template>

<script setup>
import { defineProps, ref } from "vue";
import { Confirm } from "@/components/pro-confirm/index.js";
import WorkFlowAttachmentOperator from "./operator.vue";
import WorkFlowAttachmentRunning from "./running/index.vue";
import WorkFlowAttachmentList from "./list.vue";

import { deleteAttachmentApi } from "@/api/attachment.js";
import { generateUUID } from "./options";

const ATTACHMENT_TYPE = {
  VIEW: "view",
  EDIT: "edit",
};

defineProps({
  formId: {
    type: String,
    default: "",
  },
  size: {
    type: String,
    default: "middle", // middle、small
  },
  type: {
    type: String,
    default: "edit", // edit、view
  },
});

const listRef = ref(null);
const runningUploadFiles = ref([]);
const checkData = ref([]);

const uploadFiles = (files) => {
  files.forEach((file) => {
    runningUploadFiles.value.push({
      // 新增任务
      file,
      uuid: generateUUID(),
    });
  });
};

const deleteFiles = () => {
  if (!checkData.value.length) {
    Confirm.error("请至少选中一项数据");
    return;
  }

  Confirm.warning({
    content: "是否删除选中数据?",
    async confirm() {
      await deleteAttachmentApi({
        ids: checkData.value.join(),
      });
      onComplate();
    },
  });
};

const onComplate = () => {
  listRef.value?.reload?.();
};
</script>

<style lang="less" scoped>
.work-flow-attachment {
  padding: 12px 24px;
}
</style>

上传完成和上传取消(利用v-model=""语法糖更改runningUploadFiles数据):

running 下 index.vue代码

vue
<template>
  <div class="work-flow-attachment-running">
    <AttachmentRunningItem
      v-for="item in modelValue"
      :key="item.uuid"
      v-bind="item"
      :formId="formId"
      @cancel="onCancel"
      @success="onSuccess"
    />
  </div>
</template>

<script setup>
import { defineProps, defineEmits } from "vue";
import AttachmentRunningItem from "./item.vue";

const emit = defineEmits(["update:modelValue", "complate"]);

const props = defineProps({
  modelValue: {
    type: Array,
    default: () => [],
  },
  formId: {
    type: String,
    default: "",
  },
});

// 清除数据
const onCancel = (uuid) => {
  emit(
    "update:modelValue",
    props.modelValue.filter((item) => item.uuid !== uuid),
  );
};

// 完成
const onSuccess = (uuid) => {
  onCancel(uuid);
  emit("complate");
};
</script>

<style lang="less" scoped>
.work-flow-attachment-running {
  > div:last-child {
    margin-bottom: 16px;
  }
}
</style>
  1. 删除列表相关实现也是利用**v-model="" 语法糖更新checkData数据:**

list.vue代码:

vue
<template>
  <div class="work-flow-attachment-list">
    <div
      v-for="(item, idx) in listData"
      :key="idx"
      class="attachment-list__item"
    >
      <ProCheckbox v-model="listData[idx].check" @change="onCheckboxChange">
        <span
          class="attachment-list__item--content"
          @click.stop="onDownload(item.id)"
        >
          <span class="attachment-list__item--title">
            <span>{{ item.name }}</span>
            <span>[{{ item.size }}]</span>
          </span>

          <span
            class="attachment-list__item--operator"
            @click.stop="onDownload(item.id)"
          >
            <a-tooltip placement="top">
              <template #title>
                <span>下载</span>
              </template>
              <download-outlined />
            </a-tooltip>
          </span>

          <span
            class="attachment-list__item--operator"
            v-if="isVerifySupportPreview(item.name)"
            @click.stop="onPreview(item.id)"
          >
            <a-tooltip placement="top">
              <template #title>
                <span>预览</span>
              </template>
              <folder-view-outlined />
            </a-tooltip>
          </span>
        </span>
      </ProCheckbox>
    </div>
  </div>
</template>

<script setup>
import {
  defineProps,
  onMounted,
  ref,
  defineEmits,
  defineExpose,
  watch,
} from "vue";
import ProCheckbox from "@/components/pro-checkbox/index.vue";
import { getAttachmentListApi } from "@/api/attachment.js";
import { convertFileSize } from "./options";

const isSupportPreview = [
  ".png",
  ".ofd",
  ".xls",
  ".xlsx",
  ".jpg",
  ".doc",
  ".docx",
  ".pdf",
];

const emit = defineEmits(["update:modelValue", "complate"]);
const props = defineProps({
  formId: {
    type: String,
    default: "",
  },
  modelValue: {
    type: Array,
    default: () => [],
  },
});

const listData = ref([]);

const isVerifySupportPreview = (name = "") => {
  return isSupportPreview.some((item) => name.endsWith(item));
};

watch(
  () => props.formId,
  () => {
    getData();
  },
);

const getData = async () => {
  if (!props.formId) return;
  const response = await getAttachmentListApi({ formId: props.formId });

  listData.value = (response?.result || []).map((item) => {
    return {
      name: item.fileName,
      suffix: item.fileSuffix,
      id: item.id,
      size: convertFileSize(item.totalSpace),
      check: false,
    };
  });
};

const onDownload = (id) => {};

const onPreview = () => {};

const reload = getData;

const onCheckboxChange = () => {
  emit(
    "update:modelValue",
    listData.value.filter((item) => item.check).map((item) => item.id),
  );
};

onMounted(getData);

defineExpose({
  reload,
});
</script>

<style lang="less" scoped>
.work-flow-attachment-list {
  .attachment-list__item {
    margin-top: 8px;
  }

  .attachment-list__item--content {
    .attachment-list__item--title {
      &:hover {
        color: #3080f8;
      }
    }

    .attachment-list__item--operator {
      font-size: 16px;
      &:hover {
        color: #3080f8;
      }
    }

    span {
      margin-right: 4px;
    }
  }
}
</style>

总结

组件设计好,职责划分明确后在进行编码能使代码更好维护,更利于迭代。 咱们在划分完成后只关心数据runningUploadFiles以及checkData处理就行了。希望这个文章对你来说能学习到:

  • 文件进度条获取
  • 文件上传取消
  • 利用v-model语法糖使代码数据流转更便捷清晰
  • 数据模块的设计划分