Vue3项目Easy云盘(二):文件列表+新建目录+文件重命名+文件上传

一、文件列表

1.封装全局组件Table.vue

因为Main.vue等都会用到文件列表table,所以直接封装成组件。

src/components/Table.vue

<template>
  <!-- 表格 -->
  <div>
    <el-table
      ref="dataTable"
      :data="dataSource.list || []"
      :height="tableHeight"
      :stripe="options.stripe"
      :border="options.border"
      header-row-class-name="table-header-row"
      highlight-current-row
      @row-click="handleRowClick"
      @selection-change="handleSelectionChange"
    >
      <!-- selection 选择框 -->
      <el-table-column
        v-if="options.selectType && options.selectType == 'checkbox'"
        type="selection"
        width="50"
        align="center"
      ></el-table-column>
      <!-- 序号 -->
      <el-table-column
        v-if="options.showIndex"
        label="序号"
        type="index"
        width="60"
        align="center"
      ></el-table-column>
      <!-- 数据列 -->
      <template v-for="(column, index) in columns">
        <!-- 如果数据列中有插槽, 将其改造成插槽 -->
        <template v-if="column.scopedSlots">
          <el-table-column
            :key="index"
            :prop="column.prop"
            :label="column.label"
            :align="column.align || 'left'"
            :width="column.width"
          >
            <template #default="scope">
              <slot
                :name="column.scopedSlots"
                :index="scope.$index"
                :row="scope.row"
              >
              </slot>
            </template>
          </el-table-column>
        </template>
        <!-- 如果不是插槽,就正常操作 -->
        <template v-else>
          <el-table-column
            :key="index"
            :prop="column.prop"
            :label="column.label"
            :align="column.align || 'left'"
            :width="column.width"
            :fixed="column.fixed"
          >
          </el-table-column>
        </template>
      </template>
    </el-table>

    <!-- 分页 -->
    <!-- page-sizes 每页显示个数选择器的选项设置 -->
    <!-- page-size 每页显示条目个数 -->
    <!-- current-page 当前页数 -->
    <!-- layout	组件布局,子组件名用逗号分隔 -->
    <!-- size-change page-size 改变时触发 -->
    <!-- current-change	current-page 改变时触发 -->
    <div class="pagination" v-if="showPagination">
      <el-pagination
        v-if="dataSource.totalCount"
        background
        :total="dataSource.totalCount"
        :page-sizes="[15, 30, 50, 100]"
        :page-size="dataSource.pageSize"
        :current-page.sync="dataSource.pageNo"
        :layout="layout"
        @size-change="handlePageSizeChange"
        @current-change="handlePageNoChange"
        style="text-align: right"
      ></el-pagination>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from "vue";

// 将选中的行传递给父组件Main
const emit = defineEmits(["rowSelected", "rowClick"]);
// 子组件接受父组件的值
const props = defineProps({
  dataSource: Object,
  showPagination: {
    type: Boolean,
    default: true,
  },
  showPageSize: {
    type: Boolean,
    default: true,
  },
  options: {
    type: Object,
    default: {
      extHeight: 0,
      showIndex: false,
    },
  },
  columns: Array,
  fetch: Function, // 获取数据的函数
  initFetch: {
    type: Boolean,
    default: true,
  },
});

// 分页处布局
const layout = computed(() => {
  return `total, ${
    props.showPageSize ? "sizes" : ""
  }, prev, pager, next, jumper`;
});

// 计算顶部高度
//顶部 60 , 内容区域距离顶部 20, 内容上下内间距 15*2  分页区域高度 46
const topHeight = 60 + 20 + 30 + 46;

// 计算当前表格高度,实现页面内部滚动
const tableHeight = ref(
  props.options.tableHeight
    ? props.options.tableHeight
    : window.innerHeight - topHeight - props.options.extHeight
);

const init = () => {
  if (props.initFetch && props.fetch) {
    // 获取数据
    props.fetch();
  }
};
init();

const dataTable = ref();
// 清除选中
const clearSelection = () => {
  dataTable.value.clearSelection();
};
// 设置行选中
const setCurrentRow = (rowKey, rowValue) => {
  let row = props.dataSource.list.find((item) => {
    return item[rowKey] === rowValue;
  });
  dataTable.value.setCurrentRow(row);
};
// 将父组件最新的行信息更新到子组件中
// 将子组件暴露出去,否则无法调用
defineExpose({ setCurrentRow, clearSelection });

// 行点击
const handleRowClick = (row) => {
  emit("rowClick", row);
};
// 行选中(多行)
const handleSelectionChange = (row) => {
  emit("rowSelected", row);
};

// 切换每页大小
const handlePageSizeChange = (size) => {
  props.dataSource.pageSize = size;
  props.dataSource.pageNo = 1;
  // 获取数据
  props.fetch();
};

// 切换页码
const handlePageNoChange = (pageNo) => {
  props.dataSource.pageNo = pageNo;
  // 获取数据
  props.fetch();
};
</script>

<style lang="scss" scoped>
.pagination {
  padding-top: 10px;
  padding-right: 10px;
}
.el-pagination {
  justify-content: right;
}

:deep .el-table__cell {
  padding: 4px 0px;
}
</style>

2.封装全局组件Icon.vue

因为需要展示上传文件,文件夹,图片,视频等的缩略图,所以,在Icon组件里面直接定义好各种类型显示的缩略图。

src/components/Icon.vue

<template>
  <!-- 图标 -->
  <span :style="{ width: width + 'px', height: width + 'px' }" class="icon">
    <img :src="getImage()" :style="{ 'object-fit': fit }" />
  </span>
</template>

<script setup>
import { ref, reactive, getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
const props = defineProps({
  fileType: {
    type: Number,
  },
  iconName: {
    type: String,
  },
  cover: {
    type: String,
  },
  width: {
    type: Number,
    default: 32,
  },
  fit: {
    type: String,
    default: "cover",
  },
});

const fileTypeMap = {
  0: { desc: "目录", icon: "folder" },
  1: { desc: "视频", icon: "video" },
  2: { desc: "音频", icon: "music" },
  3: { desc: "图片", icon: "image" },
  4: { desc: "exe", icon: "pdf" },
  5: { desc: "doc", icon: "word" },
  6: { desc: "excel", icon: "excel" },
  7: { desc: "纯文本", icon: "txt" },
  8: { desc: "程序", icon: "code" },
  9: { desc: "压缩包", icon: "zip" },
  10: { desc: "其他文件", icon: "others" },
};

const getImage = () => {
  // 当上传的不是本地文件,而是服务器上转码之后的图片或者视频
  if (props.cover) {
    return proxy.globalInfo.imageUrl + props.cover;
  }
  let icon = "unknow_icon";
  // 根据文件名判断图标
  if (props.iconName) {
    icon = props.iconName;
  } else {
    // 根据文件类型判断图标
    const iconMap = fileTypeMap[props.fileType];
    if (iconMap != undefined) {
      icon = iconMap["icon"];
    }
  }
  return new URL(`/src/assets/icon-image/${icon}.png`, import.meta.url).href;
};
</script>

<style lang="scss" scoped>
.icon {
  text-align: center;
  display: inline-block;
  border-radius: 3px;
  overflow: hidden;
  img {
    width: 100%;
    height: 100%;
  }
}
</style>

3.main.js引入全局组件


import Table from '@/components/Table.vue'
import Icon from '@/components/Icon.vue'


app.component("Table",Table)
app.component("Icon",Icon)

4.封装文件列表样式组件file.list.scss

包括头部top,文件列表样式file-list,没有数据样式no-data

src/assets/file.list.scss

.top {
    margin-top: 20px;
    .top-op {
        display: flex;
        align-items: center;
        .btn {
            margin-right: 10px;
        }
        .search-panel {
            margin-left: 10px;
            width: 300px;
        }
        .icon-refresh {
            cursor: pointer;
            margin-left: 10px;
        }
        .not-allow {
            background: #d2d2d2 !important;
            cursor: not-allowed;
        }
    }
}

.file-list {
    .file-item {
        display: flex;
        align-items: center;
        padding: 6px 0px;
        .file-name {
            margin-left: 8px;
            flex: 1;
            width: 0;
            overflow: hidden;
            // 当对象内文本溢出时显示省略标记(...)
            text-overflow: ellipsis;
            // 不换行 强行文本在同一行显示
            white-space: nowrap;
            span {
                cursor: pointer;
                &:hover {
                    color: #06a7ff;
                }
            }
            .transfer-status {
                font-size: 13px;
                margin-left: 10px;
                color: #e6a23c;
            }
            .transfer-fail {
                color: #f75000;
            }
        }
        .edit-panel {
            flex: 1;
            width: 0;
            display: flex;
            align-items: center;
            margin: 0px 5px;
            .iconfont {
                margin-left: 10px;
                background: #0c95f7;
                color: #fff;
                padding: 3px 5px;
                border-radius: 5px;
                cursor: pointer;
            }
            .not-allow {
                cursor: not-allowed;
                background: #7cb1d7;
                color: #ddd;
                text-decoration: none;
            }
        }
        .op {
            width: 280px;
            margin-left: 15px;
            .iconfont {
                font-size: 13px;
                margin-left: 5px;
                color: #06a7ff;
                cursor: pointer;
            }
            .iconfont::before {
                margin-right: 1px;
            }
        }
    }
}

// justify-content 设置主轴上的子元素排列方式
// align-content 设置侧轴上的子元素的排列方式(多行)
.no-data {
    // vh就是当前屏幕可见高度的1%
    // height:100vh == height:100%;
    // calc(100vh - 150px)表示整个浏览器窗口高度减去150px的大小
    height: calc(100vh - 150px);
    display: flex;
    // align-items 设置侧轴上的子元素的排列方式(单行)
    align-items: center;
    // 设置主轴上的子元素排列方式
    justify-content: center;
    .no-data-inner {
        text-align: center;
        .tips {
            margin-top: 10px;
        }
        .op-list {
            margin-top: 20px;
            display: flex;
            justify-content: center;
            align-items: center;
            .op-item {
                cursor: pointer;
                width: 100px;
                height: 100px;
                margin: 0px 10px;
                padding: 5px 0px;
                background: rgb(241, 241, 241);
            }
        }
    }
}

src/views/main/Main.vue引入

@import "@/assets/file.list.scss"


5.文件列表搭建

完整版Main.vue

src/views/main/Main.vue

<template>
  <div>
    <div class="top">
      <!-- 头部按钮处 -->
      <div class="top-op">
        <div class="btn">
          <!-- show-file-list	是否显示已上传文件列表 -->
          <!-- with-credentials	支持发送 cookie 凭证信息 -->
          <!-- multiple	是否支持多选文件 -->
          <!-- http-request	覆盖默认的 Xhr 行为,允许自行实现上传文件的请求 -->
          <!-- accept	接受上传的文件类型 -->
          <el-upload
            :show-file-list="false"
            :with-credentials="true"
            :multiple="true"
            :http-request="addFile"
            :accept="fileAccept"
          >
            <el-button type="primary">
              <span class="iconfont icon-upload"></span>
              &nbsp上传
            </el-button>
          </el-upload>
        </div>
        <el-button type="success" @click="newFolder" v-if="category == 'all'">
          <span class="iconfont icon-folder-add"></span>
          &nbsp新建文件夹
        </el-button>
        <el-button
          @click="delFileBatch"
          type="danger"
          :disabled="selectFileIdList.length == 0"
        >
          <span class="iconfont icon-del"></span>
          &nbsp批量删除
        </el-button>
        <el-button
          @click="moveFolderBatch"
          type="warning"
          :disabled="selectFileIdList.length == 0"
        >
          <span class="iconfont icon-move"></span>
          &nbsp批量移动
        </el-button>
        <div class="search-panel">
          <el-input
            clearable
            placeholder="请输入文件名搜索"
            v-model="fileNameFuzzy"
            @keyup.enter="search"
          >
            <template #suffix>
              <i class="iconfont icon-search" @click="search"></i>
            </template>
          </el-input>
        </div>
        <div class="iconfont icon-refresh" @click="loadDataList"></div>
      </div>
      <!-- 导航 -->
      <Navigation ref="navigationRef" @navChange="navChange"></Navigation>
    </div>

    <!-- 文件列表 -->
    <div class="file-list" v-if="tableData.list && tableData.list.length > 0">
      <Table
        ref="dataTableRef"
        :columns="columns"
        :showPagination="true"
        :dataSource="tableData"
        :fetch="loadDataList"
        :initFetch="false"
        :options="tableOptions"
        @rowSelected="rowSelected"
      >
        <!-- 文件名 -->
        <template #fileName="{ index, row }">
          <!-- showOp(row) 当鼠标放在当前行时,分享下载等图标出现 -->
          <!-- cancelShowOp(row) 当鼠标离开当前行时,分享下载等图标消失 -->
          <div
            class="file-item"
            @mouseenter="showOp(row)"
            @mouseleave="cancelShowOp(row)"
          >
            <!-- 显示文件图标 -->
            <template
              v-if="(row.fileType == 3 || row.fileType == 1) && row.status == 2"
            >
              <!-- 如果文件类型是图片或者视频,且已经成功转码,则执行 Icon中的cover -->
              <Icon :cover="row.fileCover" :width="32"></Icon>
            </template>
            <template v-else>
              <!-- 如果文件夹类型是文件,则文件类型是该文件类型 -->
              <Icon v-if="row.folderType == 0" :fileType="row.fileType"></Icon>
              <!-- 如果文件夹类型是目录,则文件类型就是目录0 -->
              <Icon v-if="row.folderType == 1" :fileType="0"></Icon>
            </template>

            <!-- 显示文件名称 -->
            <!-- v-if="!row.showEdit" 如果该行文件没有编辑 -->
            <span class="file-name" v-if="!row.showEdit" :title="row.fileName">
              <span @click="preview(row)">{{ row.fileName }}</span>
              <span v-if="row.status == 0" class="transfer-status">转码中</span>
              <span v-if="row.status == 1" class="transfer-status transfer-fail"
                >转码失败</span
              >
            </span>

            <!-- 点击新建文件夹时显示行 -->
            <div class="edit-panel" v-if="row.showEdit">
              <el-input
                v-model.trim="row.fileNameReal"
                ref="editNameRef"
                :maxLength="190"
                @keyup.enter="saveNameEdit(index)"
              >
                <template #suffix>{{ row.fileSuffix }}</template>
              </el-input>

              <!-- 对号 确定 -->
              <span
                :class="[
                  'iconfont icon-right1',
                  row.fileNameReal ? '' : 'not-allow',
                ]"
                @click="saveNameEdit(index)"
              ></span>

              <!-- 叉号 取消 -->
              <span
                class="iconfont icon-error"
                @click="cancelNameEdit(index)"
              ></span>
            </div>

            <!-- 当鼠标放在当前行时显示 -->
            <span class="op">
              <template v-if="row.showOp && row.fileId && row.status == 2">
                <span class="iconfont icon-share1" @click="share(row)">
                  分享
                </span>
                <!-- 只有当是文件夹时才可下载 -->
                <span
                  class="iconfont icon-download"
                  v-if="row.folderType == 0"
                  @click="download(row)"
                >
                  下载
                </span>
                <span class="iconfont icon-del" @click="delFile(row)">
                  删除
                </span>
                <span class="iconfont icon-edit" @click="editFileName(index)">
                  重命名
                </span>
                <span class="iconfont icon-move" @click="moveFolder(row)">
                  移动
                </span>
              </template>
            </span>
          </div>
        </template>

        <!-- 文件大小 -->
        <template #fileSize="{ index, row }">
          <span v-if="row.fileSize">
            {{ proxy.Utils.size2Str(row.fileSize) }}</span
          >
        </template>
      </Table>
    </div>
    
    <div class="no-data" v-else>
      <div class="no-data-inner">
        <Icon iconName="no_data" :width="120" fit="fill"></Icon>
        <div class="tips">当前目录为空,上传你的第一个文件吧</div>
        <div class="op-list">
          <el-upload
            :show-file-list="false"
            :with-credentials="true"
            :multiple="true"
            :http-request="addFile"
            :accept="fileAccept"
          >
            <div class="op-item">
              <Icon iconName="file" :width="60"></Icon>
              <div>上传文件</div>
            </div>
          </el-upload>
          <div class="op-item" v-if="category == 'all'" @click="newFolder">
            <Icon iconName="folder" :width="60"></Icon>
            <div>新建目录</div>
          </div>
        </div>
      </div>
    </div>
    <FolderSelect
      ref="folderSelectRef"
      @folderSelect="moveFolderDone"
    ></FolderSelect>

    <!-- 预览 -->
    <Preview ref="previewRef"></Preview>

    <!-- 分享 -->
    <ShareFile ref="shareRef"></ShareFile>
  </div>
</template>

<script setup>
import CategoryInfo from "@/js/CategoryInfo.js";
import ShareFile from "./ShareFile.vue";

import { ref, reactive, getCurrentInstance, nextTick, computed } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();

// 实现上传文件的请求
// 将Main子组件页面的数据传递给Framwork父组件
const emit = defineEmits(["addFile"]);
const addFile = async (fileData) => {
  emit("addFile", { file: fileData.file, filePid: currentFolder.value.fileId });
};

// 添加文件回调
const reload = () => {
  showLoading.value = false;
  loadDataList();
};
defineExpose({ reload });

const api = {
  loadDataList: "/file/loadDataList",
  rename: "/file/rename",
  newFoloder: "/file/newFoloder",
  getFolderInfo: "/file/getFolderInfo",
  delFile: "/file/delFile",
  changeFileFolder: "/file/changeFileFolder",
  createDownloadUrl: "/file/createDownloadUrl",
  download: "/api/file/download",
};

// 实现文件选择
const fileAccept = computed(() => {
  const categoryItem = CategoryInfo[category.value];
  return categoryItem ? categoryItem.accept : "*";
});

// 列表头信息
const columns = [
  {
    label: "文件名",
    prop: "fileName",
    scopedSlots: "fileName",
  },
  {
    label: "修改时间",
    prop: "lastUpdateTime",
    width: 200,
  },
  {
    label: "文件大小",
    prop: "fileSize",
    scopedSlots: "fileSize",
    width: 200,
  },
];

// 搜索功能
const search = () => {
  showLoading.value = true;
  loadDataList();
};

// 数据源
const tableData = ref({});
// 表格选项
const tableOptions = {
  extHeight: 50,
  selectType: "checkbox",
};
// 文件名
const fileNameFuzzy = ref();

const showLoading = ref(true);
// 分类
const category = ref();
// 当前文件夹
const currentFolder = ref({ fileId: 0 });

// 获得数据;
const loadDataList = async () => {
  let params = {
    // 页码
    pageNo: tableData.value.pageNo,
    // 分页大小
    pageSize: tableData.value.pageSize,
    // 文件名(模糊)
    fileNameFuzzy: fileNameFuzzy.value,
    // 分类
    category: category.value,
    // 文件父id
    filePid: currentFolder.value.fileId,
  };
  if (params.category !== "all") {
    delete params.filePid;
  }
  let result = await proxy.Request({
    url: api.loadDataList,
    showLoading: showLoading,
    params,
  });
  if (!result) {
    return;
  }
  tableData.value = result.data;
  editing.value = false;
};

// 当鼠标放在当前行时,分享下载等图标出现
const showOp = (row) => {
  // 关闭所有的显示
  tableData.value.list.forEach((element) => {
    element.showOp = false;
  });
  // 只开启当前显示
  row.showOp = true;
};

const cancelShowOp = (row) => {
  row.showOp = false;
};

// 编辑行(新建文件夹时编辑行)
// 当前编辑行状态
const editing = ref(false);
// 新建文件夹行内填充的内容绑定
const editNameRef = ref();

// 新建文件夹
const newFolder = () => {
  // 如果当前编辑行存在,则再次点击新建文件夹按钮时不起作用
  if (editing.value) {
    return;
  }
  // 让其他行都不允许编辑
  tableData.value.list.forEach((element) => {
    element.showEdit = false;
  });
  editing.value = true;
  tableData.value.list.unshift({
    showEdit: true,
    fileType: 0,
    fileId: "",
    filePid: currentFolder.value.fileId,
  });
  nextTick(() => {
    editNameRef.value.focus();
  });
};

// 取消新建文件夹操作
const cancelNameEdit = (index) => {
  const fileData = tableData.value.list[index];
  // 如果存在这个文件的话,说明此处是重命名操作,那么可以直接将编辑行关闭
  if (fileData.fileId) {
    fileData.showEdit = false;
  } else {
    // 如果不存在的话,那么直接将此行删除
    tableData.value.list.splice(index, 1);
  }
  // 当前编辑行状态为:未编辑
  editing.value = false;
};

// 确定新建文件夹操作
const saveNameEdit = async (index) => {
  const { fileId, filePid, fileNameReal } = tableData.value.list[index];
  if (fileNameReal == "" || fileNameReal.indexOf("/") != -1) {
    proxy.Message.warning("文件名不能为空且不能含有斜杠");
    return;
  }
  // 重命名
  let url = api.rename;
  if (fileId == "") {
    // 当文件ID不存在时,新建目录
    url = api.newFoloder;
  }
  let result = await proxy.Request({
    url: url,
    params: {
      fileId,
      filePid: filePid,
      fileName: fileNameReal,
    },
  });
  if (!result) {
    return;
  }
  tableData.value.list[index] = result.data;
  editing.value = false;
};

// 重命名 编辑文件名
const editFileName = (index) => {
  // 如果现在有新建文件夹的编辑行,那么先将其删除,并且将序号减一
  if (tableData.value.list[0].fileId == "") {
    tableData.value.list.splice(0, 1);
    index = index - 1;
  }
  tableData.value.list.forEach((element) => {
    element.showEdit = false;
  });
  let cureentData = tableData.value.list[index];
  cureentData.showEdit = true;

  //编辑文件
  if (cureentData.folderType == 0) {
    cureentData.fileNameReal = cureentData.fileName.substring(
      0,
      cureentData.fileName.indexOf(".")
    );
    cureentData.fileSuffix = cureentData.fileName.substring(
      cureentData.fileName.indexOf(".")
    );
  } else {
    cureentData.fileNameReal = cureentData.fileName;
    cureentData.fileSuffix = "";
  }

  // 当前编辑行状态为true
  editing.value = true;
  nextTick(() => {
    editNameRef.value.focus();
  });
};

// 行选中
// 多选 批量选中
const selectFileIdList = ref([]);
const rowSelected = (rows) => {
  selectFileIdList.value = [];
  rows.forEach((item) => {
    selectFileIdList.value.push(item.fileId);
  });
};

// 删除单个文件
const delFile = (row) => {
  proxy.Confirm(
    `你确定要删除【$row.fileName】吗?删除的文件可在 10 天内通过回收站还原`,
    async () => {
      let result = await proxy.Request({
        url: api.delFile,
        params: {
          fileIds: row.fileId,
        },
      });
      if (!result) {
        return;
      }
      // 重新获取数据
      loadDataList();
    }
  );
};

// 批量删除文件
const delFileBatch = () => {
  if (selectFileIdList.value.length == 0) {
    return;
  }
  proxy.Confirm(
    `你确定要删除这些文件吗?删除的文件可在 10 天内通过回收站还原`,
    async () => {
      let result = await proxy.Request({
        url: api.delFile,
        params: {
          fileIds: selectFileIdList.value.join(","),
        },
      });
      if (!result) {
        return;
      }
      // 重新获取数据
      loadDataList();
    }
  );
};

// 移动目录
const folderSelectRef = ref();
// 当前要移动的文件(单个文件)
const currentMoveFile = ref({});

// 移动单个文件
const moveFolder = (data) => {
  currentMoveFile.value = data;
  folderSelectRef.value.showFolderDialog(currentFolder.value.fileId);
};

// 移动批量文件
const moveFolderBatch = () => {
  currentMoveFile.value = {};
  folderSelectRef.value.showFolderDialog(currentFolder.value.fileId);
};

// 移动文件操作
const moveFolderDone = async (folderId) => {
  // 如果要移动到当前目录,提醒无需移动
  if (
    currentMoveFile.value.filePid == folderId ||
    currentFolder.value.fileId == folderId
  ) {
    proxy.Message.warning("文件正在当前目录,无需移动");
    return;
  }
  let filedIdsArray = [];
  // 如果是单个文件移动
  if (currentMoveFile.value.fileId) {
    filedIdsArray.push(currentMoveFile.value.fileId);
  } else {
    // 如果是多个文件移动
    // concat 连接多个数组
    // selectFileIdList 是指批量选择时选择的文件ID
    filedIdsArray = filedIdsArray.concat(selectFileIdList.value);
  }
  let result = await proxy.Request({
    url: api.changeFileFolder,
    params: {
      fileIds: filedIdsArray.join(","),
      filePid: folderId,
    },
  });
  if (!result) {
    return;
  }
  // 调用子组件暴露的close方法,实现当前弹出框页面的关闭
  folderSelectRef.value.close();
  // 更新当前文件列表
  loadDataList();
};

// 绑定导航栏
const navigationRef = ref();

// 预览
const previewRef = ref();
const preview = (data) => {
  // 如果是目录(文件夹)
  if (data.folderType == 1) {
    navigationRef.value.openFolder(data);
    return;
  }
  if (data.status != 2) {
    proxy.Message.warning("文件未完成转码,无法预览");
    return;
  }
  previewRef.value.showPreview(data, 0);
};

// 目录
const navChange = (data) => {
  const { curFolder, categoryId } = data;
  currentFolder.value = curFolder;
  showLoading.value = true;
  category.value = categoryId;
  loadDataList();
};

// 下载文件
const download = async (row) => {
  let result = await proxy.Request({
    url: api.createDownloadUrl + "/" + row.fileId,
  });

  if (!result) {
    return;
  }

  window.location.href = api.download + "/" + result.data;
};

// 分享文件
// 利用ShareFile组件暴露出的show函数,实现将Main组件中的函数传递给ShareFile组件
const shareRef = ref();
const share = (row) => {
  shareRef.value.show(row);
};
</script>

<style lang="scss" scoped>
@import "@/assets/file.list.scss";
</style>

二、功能实现

功能:新建目录,文件上传,分享,下载,删除,重命名,移动

1.将文件以字节为单位的转换为其他单位,封装组件


src/utils/Utils.js

// 将文件以字节为单位的转换为其他单位
export default {
    size2Str: (limit) => {
        var size = "";
        if (limit < 0.1 * 1024) { //小于0.1KB,则转化成B
            size = limit.toFixed(2) + "B"
        } else if (limit < 0.1 * 1024 * 1024) { //小于0.1MB,则转化成KB
            size = (limit / 1024).toFixed(2) + "KB"
        } else if (limit < 0.1 * 1024 * 1024 * 1024) { //小于0.1GB,则转化成MB
            size = (limit / (1024 * 1024)).toFixed(2) + "MB"
        } else { //其他转化成GB
            size = (limit / (1024 * 1024 * 1024)).toFixed(2) + "GB"
        }
        var sizeStr = size + ""; //转成字符串
        var index = sizeStr.indexOf("."); //获取小数点处的索引
        var dou = sizeStr.substr(index + 1, 2) //获取小数点后两位的值
        if (dou == "00") { //判断后两位是否为00,如果是则删除00               
            return sizeStr.substring(0, index) + sizeStr.substr(index + 3, 2)
        }
        return size;
    },
}

main.js引入


import Utils from './utils/Utils'

app.config.globalProperties.Utils=Utils

2.新建目录功能

<!-- 按钮2 -->
                <el-button type="success" @click="newFolder">
                    <span class="iconfont icon-folder-add"></span>
                    新建文件夹
                </el-button>

回调:

// 编辑行(新建文件夹时编辑行)
// 当前编辑行状态
const editing = ref(false);
// 新建文件夹行内填充的内容绑定
const editNameRef = ref();
// 新建文件夹
const newFolder = () => {
    // 如果当前编辑行存在,则再次点击新建文件夹按钮时不起作用
    // 确保在编辑现有项目时,不能同时开始编辑新的项目。
    if (editing.value) {
        return;
    }
    // 让其他行都不允许编辑
    tableData.value.list.forEach((element) => {
        element.showEdit = false;
    });
    // 表示现在有一个项目正在被编辑
    editing.value = true;
    // 在列表顶部添加新文件夹:
    tableData.value.list.unshift({
        showEdit: true,
        fileType: 0,
        fileId: "",
        filePid: currentFolder.value.fileId,// 父文件夹的ID
    });
    // 在下一个“tick”中将焦点设置到某个输入框:
    nextTick(() => {
        editNameRef.value.focus();
    });
};
// 取消新建文件夹操作
const cancelNameEdit = (index) => {
    const fileData = tableData.value.list[index];
    // 如果存在这个文件的话,说明此处是重命名操作,那么可以直接将编辑行关闭
    if (fileData.fileId) {
        fileData.showEdit = false;
    } else {
        // 如果不存在的话,那么直接将此行删除
        // 删除位于 index 位置的一个项目。删除后,数组的长度将减少1,并且所有高于 index 的元素都会向下移动一个位置。
        tableData.value.list.splice(index, 1);
    }
    // 当前编辑行状态为:未编辑
    editing.value = false;
};
// 确定新建文件夹操作
const saveNameEdit = async (index) => {
    // 使用解构赋值从tableData.value.list数组中的指定索引位置获取fileId、filePid和fileNameReal。
    const { fileId, filePid, fileNameReal } = tableData.value.list[index];
    // 如果文件名fileNameReal为空或包含斜杠(/),则显示警告并退出函数。
    if (fileNameReal == "" || fileNameReal.indexOf("/") != -1) {
        proxy.Message.warning("文件名不能为空且不能含有斜杠");
        return;
    }
    // 如果fileId为空,表示这是新建目录而不是重命名,所以将请求的URL设置为api.newFoloder;否则,使用默认的api.rename来重命名现有文件或文件夹。
    // 重命名
    let url = api.rename;
    if (fileId == "") {
        // 当文件ID不存在时,新建目录
        url = api.newFoloder;
    }
    // 使用proxy.Request发送一个异步请求,该请求包含URL和要传递的参数(如fileId、filePid和fileName)。这里假设proxy.Request是一个返回Promise的函数,用于发送HTTP请求。
    let result = await proxy.Request({
        url: url,
        params: {
            fileId,
            filePid: filePid,
            fileName: fileNameReal,
        },
    });
    // 如果请求没有成功(例如,返回null或undefined),则直接退出函数。
    if (!result) {
        return;
    }
    // 如果请求成功,使用响应中的数据更新tableData.value.list数组中的相应项。
    tableData.value.list[index] = result.data;
    // 关闭编辑状态
    editing.value = false;
};

3.文件重命名


<span class="iconfont icon-edit" @click="editFileName(index)">
重命名 
</span>

回调:

// 重命名 编辑文件名
const editFileName = (index) => {
    // 如果现在有新建文件夹的编辑行
    if (tableData.value.list[0].fileId == "") {
        // 那么先将其删除
        tableData.value.list.splice(0, 1);
        // 并且将序号减一,否则重命名会出错顺序
        index = index - 1;
    }
    tableData.value.list.forEach((element) => {
        // 默认情况下所有行都不显示编辑状态。
        element.showEdit = false;
    });
    // 获取要编辑的行的数据(根据传入的index)
    let cureentData = tableData.value.list[index];
    // 表示该行现在处于编辑状态
    cureentData.showEdit = true;

    //编辑文件
    if (cureentData.folderType == 0) {
        // 如果folderType为0,表示这是一个文件(或不是文件夹)
        // 使用substring和indexOf方法从文件名中提取文件名(不带后缀)和文件后缀
        cureentData.fileNameReal = cureentData.fileName.substring(
            0,
            cureentData.fileName.indexOf(".")
        );
        cureentData.fileSuffix = cureentData.fileName.substring(
            cureentData.fileName.indexOf(".")
        );
        // 如果不是文件
    } else {
        // 直接将文件名赋给fileNameReal
        cureentData.fileNameReal = cureentData.fileName;
        // 没有后缀
        cureentData.fileSuffix = "";
    }

    // 当前编辑行状态为true
    editing.value = true;
    nextTick(() => {
        editNameRef.value.focus();
    });
};

三、文件上传功能

(1)在子组件Main.vue里面定义,但是实际上传功能在Framework.vue中

<el-upload :show-file-list="false" :with-credentials="true" :multiple="true" :http-request="addFile"
                        :accept="fileAccept">
                        <el-button type="primary">
                            <span class="iconfont icon-upload"></span>
                            上传
                        </el-button>
                    </el-upload>

(2)Main.vue中

上传按钮方法::http-request="addFile"
回调:

// 实现上传文件的请求
// 定义了一个名为 addFile 的事件,该事件可以被外部(如父组件)监听。
const emit = defineEmits(["addFile"]);
// 它接收一个 fileData 参数,并使用之前定义的 emit 函数来触发一个 addFile 事件。事件传递的数据是一个对象,包含 file(从 fileData.file 获取)和 filePid(从 currentFolder.value.fileId 获取)。
const addFile = async (fileData) => {
    emit("addFile", { file: fileData.file, filePid: currentFolder.value.fileId });
};
// 当前文件夹
// currentFolder 引用用于存储当前文件夹的 ID,这个 ID 可能会随着用户操作而改变
const currentFolder = ref({ fileId: 0 });

(3)Framework.vue中

气泡框:v-model:visible="showUploader"

<!-- v-slot="{ Component } 解构插槽 -->
            <!-- 让router-view的插槽能够访问子组件中的数据 -->
            <!-- 访问的数据就是Component -->
                <router-view v-slot="{ Component }">
                    <component @addFile="addFile" ref="routerViewRef" :is="Component"></component>
                </router-view>

(4)回调

// 控制是否展示上传区域
const showUploader = ref(false);
// 文件上传处数据绑定
const uploaderRef = ref();
// 上传文件
const addFile = (data) => {
    const { file, filePid } = data;
    showUploader.value = true;
    // 调用子组件 Uploader中暴露的 addFile函数,并将参数传递给子组件
    uploaderRef.value.addFile(file, filePid);
};

四、气泡框上传区域定义组件Uploader(重点)

(1)src/views/main/Uploader.vue

框架搭建:

1.上传标题
2.上传文件列表:文件名,文件上传进度条,文件上传状态(图标+描述+大小展示),操作按钮(不同情境),判断是否有上传文件(记得引入NoData图标组件)。

<template>
  <div class="uploader-panel">
    <!-- 上传标题 -->
    <div class="uploader-title">
      <span>上传任务</span>
      <span class="tips">(仅展示本次上传任务)</span>
    </div>
    <!-- 上传列表 -->
    <div class="file-list">
      <!-- 遍历上传的每一项 -->
      <div v-for="(item, index) in fileList" class="file-item">
        <!-- 上传的每一项 -->
        <div class="upload-panel">
          <!-- 文件名 -->
          <div class="file-name">{{ item.fileName }}</div>
          <!-- 上传进度条 -->
          <div class="progress">
            <!-- 当状态为 上传中/上传完成/秒传时显示 -->
            <!-- Element UI 的进度条组件,percentage 属性(进度条的百分比)绑定到 item.uploadProgress 这个数据上。 -->
            <el-progress :percentage="item.uploadProgress" v-if="item.status == STATUS.uploading.value ||
              item.status == STATUS.upload_seconds.value ||
              item.status == STATUS.upload_finish.value
              "></el-progress>
          </div>
          <!-- 下方上传状态:图标+描述 -->
          <div class="upload-status">
            <!-- 图标:✔/✖ -->
            <!-- 一个静态的 'iconfont' 和一个根据 item.status 从 STATUS 对象中获取的图标类名。 -->
            <span :class="['iconfont', 'icon-' + STATUS[item.status].icon]"
              :style="{ color: STATUS[item.status].color }">
            </span>
            <!-- 状态描述:上传中/上传完成/秒传/失败 -->
            <span
                class="status"
                :style="{ color: STATUS[item.status].color }"
                >{{
                  item.status == "fail" ? item.errorMsg : STATUS[item.status].desc
                }}
            </span>
            <!-- 上传中的大小显示,传了多少,速度 -->
            <!-- v-if表示只会在文件上传过程中显示,123kb/200mb -->
            <span
              class="upload-info"
              v-if="item.status == STATUS.uploading.value">
              {{ proxy.Utils.size2Str(item.uploadSize) }}/{{
                proxy.Utils.size2Str(item.totalSize)
              }}
            </span>
          </div>
        </div>
        <!-- 后面的操作按钮 -->
        <div class="op">
          <!-- 显示 MD5解析 信息 -->
          <!-- 解析中,圆形进度条,只在文件或数据的初始化阶段显示,而不是在整个上传过程中 -->
          <el-progress
            type="circle"
            :width="50"
            :percentage="item.md5Progress"
            v-if="item.status == STATUS.init.value"
          ></el-progress>
          <!-- 按钮 -->
          <div class="op-btn">
            <!-- 如果是上传中,提供暂停和上传两个按钮 -->
            <span v-if="item.status == STATUS.uploading.value">
              <!-- 上传按钮 -->
              <Icon
                :width="28"
                class="btn-item"
                iconName="upload"
                v-if="item.pause"
                title="上传"
                @click="startUpload(item.uid)"
              ></Icon>
              <!-- 暂停按钮 -->
              <Icon
                :width="28"
                class="btn-item"
                iconName="pause"
                title="暂停"
                @click="pauseUpload(item.uid)"
                v-else
              ></Icon>
            </span>
            <!-- 在上传过程中,不是解析&上传完成&秒传的情况下,提供删除按钮(不想传了) -->
            <Icon
              :width="28"
              class="del btn-item"
              iconName="del"
              title="删除"
              v-if="item.status != STATUS.init.value &&
                item.status != STATUS.upload_finish.value &&
                item.status != STATUS.upload_seconds.value
                "
              @click="delUpload(item.uid, index)"
            ></Icon>
            <!-- 在是上传完成/秒传的情况下,提供清除按钮 -->
            <Icon
              :width="28"
              class="clean btn-item"
              iconName="clean"
              title="清除"
              v-if="item.status == STATUS.upload_finish.value ||
                item.status == STATUS.upload_seconds.value
                "
              @click="delUpload(item.uid, index)"
            ></Icon>
          </div>
        </div>
      </div>
      <!-- 当没有文件上传时的显示 -->
      <div v-if="fileList.length == 0">
        <NoData msg="暂无上传任务"></NoData>
      </div>
    </div>
  </div>
</template>
......
<style lang="scss" scoped>
.uploader-panel {
  .uploader-title {
    border-bottom: 1px solid #ddd;
    line-height: 40px;
    padding: 0px 10px;
    font-size: 15px;

    .tips {
      font-size: 13px;
      color: rgb(169, 169, 169);
    }
  }

  .file-list {
    // 如果内容溢出,则浏览器提供滚动条。
    overflow: auto;
    padding: 10px 0px;
    min-height: calc(100vh / 2);
    max-height: calc(100vh - 120px);

    .file-item {
      position: relative;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 3px 10px;
      background-color: #fff;
      border-bottom: 1px solid #ddd;
    }

    .file-item:nth-child(even) {
      background-color: #fcf8f4;
    }

    .upload-panel {
      flex: 1;

      .file-name {
        color: rgb(64, 62, 62);
      }

      .upload-status {
        display: flex;
        align-items: center;
        margin-top: 5px;

        .iconfont {
          margin-right: 3px;
        }

        .status {
          color: red;
          font-size: 13px;
        }

        .upload-info {
          margin-left: 5px;
          font-size: 12px;
          color: rgb(112, 111, 111);
        }
      }

      .progress {
        height: 10px;
      }
    }

    .op {
      width: 100px;
      display: flex;
      align-items: center;
      justify-content: flex-end;

      .op-btn {
        .btn-item {
          cursor: pointer;
        }

        .del,
        .clean {
          margin-left: 5px;
        }
      }
    }
  }
}</style>

功能实现:

定义上传状态STATUS,
定义addFile暴露给父组件 FrameWork,方便其调用该方法defineExpose({ addFile });,
接收一个参数 uid 并返回一个与给定 uid 匹配的文件对象(如果存在的话)
计算MD5值computeMD5,
上传文件uploadFile ,

根据文件Id获取到文件getFileUid.

(下面的代码会有详细注释)

 

<script setup>
import {
  getCurrentInstance,
  onMounted,
  reactive,
  ref,
  watch,
  nextTick,
} from "vue";
import SparkMD5 from "spark-md5";
const { proxy } = getCurrentInstance();

const api = {
  upload: "/file/uploadFile",
};

// 定义不同的上传状态
const STATUS = {
  emptyfile: {
    value: "emptyfile",
    desc: "文件为空",
    color: "#F75000",
    icon: "close",
  },
  fail: {
    value: "fail",
    desc: "上传失败",
    color: "#F75000",
    icon: "close",
  },
  init: {
    value: "init",
    desc: "解析中",
    color: "#e6a23c",
    icon: "clock",
  },
  uploading: {
    value: "uploading",
    desc: "上传中",
    color: "#409eff",
    icon: "upload",
  },
  upload_finish: {
    value: "upload_finish",
    desc: "上传完成",
    color: "#67c23a",
    icon: "ok",
  },
  upload_seconds: {
    value: "upload_seconds",
    desc: "秒传",
    color: "#67c23a",
    icon: "ok",
  },
};
// 分片时,每片的大小
const chunkSize = 1024 * 1024 * 5;
// 文件列表
const fileList = ref([]);
// 删除的文件的ID
const delList = ref([]);

const addFile = async (file, filePid) => {
  const fileItem = {
    // 文件
    file: file,
    // 文件ID
    uid: file.uid,
    // md5进度(转圈进度)
    md5Progress: 0,
    // md5值
    md5: null,
    // 文件名,文件展示的名字
    fileName: file.name,
    // 上传状态
    status: STATUS.init.value,
    // 已上传大小
    uploadSize: 0,
    // 文件总大小
    totalSize: file.size,
    // 上传进度
    uploadProgress: 0,
    //暂停
    pause: false,
    // 当前分片
    chunkIndex: 0,
    // 父级ID
    filePid: filePid,
    // 错误信息
    errorMsg: null,
  };
  // 把上传文件加到上传列表前面
  fileList.value.unshift(fileItem);
  // 如果文件大小为0,
  if (fileItem.totalSize == 0) {
    // 表示为空文件状态
    fileItem.status = STATUS.emptyfile.value;
    // 退出
    return;
  }
  // 文件大小不为0,代码将尝试计算该文件的MD5值
  let md5FileUid = await computeMD5(fileItem);
  // 检测md5值是否有效
  if (md5FileUid == null) {
    return;
  }
  // 上传文件
  uploadFile(md5FileUid);
};
// 暴露给父组件 FrameWork,方便其调用该方法
defineExpose({ addFile });

// 上传文件
const emit = defineEmits(["uploadCallback"]);
// 异步函数,接收两个参数:uid(文件的唯一标识符)和 chunkIndex(要上传的切片的索引,默认为0)
const uploadFile = async (uid, chunkIndex) => {
  chunkIndex = chunkIndex ? chunkIndex : 0;
  // 获取当前文件
  let currentFile = getFileByUid(uid);
  // 计算切片数量
  const file = currentFile.file;
  const fileSize = currentFile.totalSize;
  const chunks = Math.ceil(fileSize / chunkSize);
  // 给定的 chunkIndex 开始,遍历所有切片
  for (let i = chunkIndex; i < chunks; i++) {
    // 判断如果在文件上传的过程中删除了文件,那么直接跳出循环
    // 调用 indexOf 方法来查找 uid 在 delList.value 列表中的索引
    let delIndex = delList.value.indexOf(uid);
    if (delIndex != -1) {
      // 使用 splice 方法来移除它。splice 方法接受两个参数:要开始移除的元素的索引(这里是 delIndex),以及要移除的元素数量(这里是 1,因为我们只移除一个元素)。
      delList.value.splice(delIndex, 1);
      break;
    }
    // 如果当前文件被暂停,那么直接跳出循环
    if (currentFile.pause) break;

    // 获取分片
    // start 变量表示当前数据块在原始文件中的起始字节位置
    let start = i * chunkSize;
    // 如果起始位置加上chunkSize超过了文件的总大小(fileSize),那么结束位置就是文件的总大小;否则,结束位置就是起始位置加上chunkSize
    let end = start + chunkSize >= fileSize ? fileSize : start + chunkSize;
    // 提取从start到end(不包括end)的字节范围,并返回一个新的Blob对象,该对象包含该范围内的数据。这个新的Blob对象(chunkFile)就是我们要上传的数据块。
    let chunkFile = file.slice(start, end);

    // 发起HTTP请求
    // uploadResult存储上传请求的响应结果
    let uploadResult = await proxy.Request({
      url: api.upload,//API的上传端点(URL)
      showLoading: false,
      dataType: "file",
      params: {
        file: chunkFile,//要上传的文件分块,它是一个Blob对象
        fileName: file.name,
        fileMd5: currentFile.md5,
        chunkIndex: i,
        chunks: chunks,//被分割的总片数
        fileId: currentFile.fileId,
        filePid: currentFile.filePid,
      },
      showError: false,
      // 报错
      // 它接收一个errorMsg参数,表示错误信息
      errorCallback: (errorMsg) => {
        // 然后,它将currentFile.status设置为失败状态
        currentFile.status = STATUS.fail.value;
        // 并将errorMsg保存到currentFile.errorMsg中
        currentFile.errorMsg = errorMsg;
      },
      // 进度更新
      // 接收一个event对象,该对象包含了关于上传进度的信息
      uploadProgressCallback: (event) => {
        // 从event中获取已加载的字节数loaded
        let loaded = event.loaded;
        if (loaded > fileSize) {
          // 检查已加载的字节数是否超过了文件总大小(fileSize),如果是,则将其设置为fileSize。
          loaded = fileSize;
        }
        // 更新currentFile.uploadSize为当前分块的起始位置加上已加载的字节数
        currentFile.uploadSize = i * chunkSize + loaded;
        // 计算上传进度百分比,并更新currentFile.uploadProgress
        currentFile.uploadProgress = Math.floor(
          (currentFile.uploadSize / fileSize) * 100
        );
      },
    });

    // 上传请求可能没有成功执行或返回了无效的结果
    if (uploadResult == null) {
      break;
    }
    // 更新文件信息
    currentFile.fileId = uploadResult.data.fileId;
    currentFile.status = STATUS[uploadResult.data.status].value;
    currentFile.chunkIndex = i;
    // 如果状态是秒传和上传完成,则执行以下操作
    if (
      uploadResult.data.status == STATUS.upload_seconds.value ||
      uploadResult.data.status == STATUS.upload_finish.value
    ) {
      // 上传进度条为100
      currentFile.uploadProgress = 100;
      // 上传结束后,uploaderCallback,将Framework中的列表刷新
      emit("uploadCallback");
      break;
    }
  }
};

// 计算文件的 MD5 值
// 会对大文件进行分片处理
const computeMD5 = (fileItem) => {
  let file = fileItem.file;
  // slice 分割文件
  // mozSlice 兼容firefox
  // webkitSlice 兼容webkit
  let blobSlice =
    File.prototype.slice ||
    File.prototype.mozSlice ||
    File.prototype.webkitSlice;
  // chunkSize 每片的大小
  // chunks 切片数量(向上取整)
  let chunks = Math.ceil(file.size / chunkSize);
  // 当前切片的下标为0
  let currentChunk = 0;
  // 创建SparkMD5的实例,计算MD5
  let spark = new SparkMD5.ArrayBuffer();
  // 使用 FileReader 读取文件的数据
  let fileReader = new FileReader();
  // 已删除文件的索引
  const delList = ref([]);

  // 加载数据
  // loadNext读取文件的下一个块
  let loadNext = () => {
    // 当前片段在文件中的起始字节位置
    let start = currentChunk * chunkSize;
    // 起始位置加上片段大小,超出文件的总大小(file.size),大小为file.size,不超出则将结束位置设置为文件的总大小。
    let end = start + chunkSize >= file.size ? file.size : start + chunkSize;
    // 来异步读取文件的指定片段
    fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
  };
  // 当 computeMD5 函数被调用时,它会立即开始读取文件的第一个片段。
  loadNext();

  // 使用 Promise 封装文件分片读取和 MD5 哈希值计算
  // 这个 Promise 将在文件的所有分片都被读取并计算完 MD5 哈希值后解决(resolve),并返回文件的唯一标识符(UID)
  return new Promise((resolve, reject) => {
    // 根据文件ID获取到文件
    let resultFile = getFileByUid(file.uid);
    // 当读取操作成功完成时调用
    // 当 FileReader 读取完一个文件分片后,会触发 onload 事件
    fileReader.onload = (e) => {
      // 向SparkMD5实例中添加数据
      spark.append(e.target.result); // Append array buffer
      // 切片下标+1
      currentChunk++;
      // 如果 currentChunk 小于 chunks(总分片数),则继续读取下一个分片
      // 自动分片解析
      if (currentChunk < chunks) {
        /*  console.log(
          `第${file.name},${currentChunk}分片解析完成, 开始第${
            currentChunk + 1
          } / ${chunks}分片解析`
        ); */
        // 计算当前进度百分比,并更新 resultFile.md5Progress
        let percent = Math.floor((currentChunk / chunks) * 100);
        resultFile.md5Progress = percent;
        // 再次读取数据,读取下一个文件分片
        loadNext();
      } else {
        // 如果当前切片下标不比切片数量小,说明解析到最后了
        // 调用 SparkMD5 的 end() 方法来计算最终的 MD5 哈希值
        let md5 = spark.end();
        /*  console.log(
          `MD5计算完成:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${
            file.size
          } 用时:${new Date().getTime() - time} ms`
        ); */
        // 释放 SparkMD5 实例占用的资源
        spark.destroy(); //释放缓存
        // 设置 resultFile.md5Progress 为 100,表示进度完成。
        resultFile.md5Progress = 100;
        // 设置 resultFile.status 为上传状态
        resultFile.status = STATUS.uploading.value;
        // 设置 resultFile.md5 为计算得到的 MD5 哈希值
        resultFile.md5 = md5;
        // 调用 resolve(fileItem.uid); 来解决 Promise,并返回文件的 UID
        resolve(fileItem.uid);
      }
    };
    // 当读取操作发生错误时调用
    fileReader.onerror = () => {
      // 将 resultFile 对象的 md5Progress 属性设置为 -1,表示 MD5 计算过程遇到了错误。
      resultFile.md5Progress = -1;
      // 设置文件状态为失败
      resultFile.status = STATUS.fail.value;
      
      resolve(fileItem.uid);
    };
    // Promise 链的一个捕获处理器(catch handler),用于处理 Promise 链中任何地方的错误
  }).catch((error) => {
    return null;
  });
};

// 根据文件ID获取到文件
const getFileByUid = (uid) => {
  let file = fileList.value.find((item) => {
    return item.file.uid === uid;
  });
  return file;
};
</script>

(2)使用组件,Framework.vue中

<template #default>
                        这里是上传区域
                        <Uploader ref="uploaderRef" @uploadCallback="uploadCallbackHandler"></Uploader>
</template>

引入

import Uploader from "@/views/main/Uploader.vue";

回调:

// 上传文件回调
const uploadCallbackHandler = () => {
    nextTick(() => {
        // 它首先等待DOM更新完成(通过nextTick)
        // 然后重新加载一个组件(可能是router-view)
        routerViewRef.value.reload();
        // 并最后调用一个函数来获取空间使用情况。
        getUseSpace();
    });
};

效果:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/619805.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于AIoTedge+ThingsKit物联网平台,实现办公室人员进出AI统计

在AIoT时代&#xff0c;智能办公已成为提升企业效率的关键。本期文章将带你了解如何利用AIoTedge结合ThingsKit物联网平台&#xff0c;实现办公室人员进出的智能统计。这不是简单的技术堆砌&#xff0c;而是一场关于AI与IoT融合的实战演示。&#x1f31f; 提示&#xff1a;AIoT…

【Linux】Centos7安装部署unimrcp,搭建MRCP服务器

yum install libtool yum install libtool-ltdl-devel yum install libsofia-sip-ua find / -name libsofia-sip-ua.so.0 2>/dev/null # 设置环境变量&#xff1a;如果库文件存在但不在默认搜索路径中&#xff0c;你可以通过设置 LD_LIBRARY_PATH 环境变量来告诉系统在哪…

Github学习

1.Git与Github 区别: Git是一个分布式版本控制系统&#xff0c;简单的说就是一个软件&#xff0c;用于记录一个或若干个文件内容变化&#xff0c;以便将来查阅特点版本修订情况的软件。 Github是一个为用户提高Git服务的网站&#xff0c;简单说就是一个可以放代码的地方。Gi…

数字化社会的引擎:揭示Facebook的影响力

在当今数字化社会中&#xff0c;社交媒体平台扮演着至关重要的角色&#xff0c;而Facebook作为其中的巨头之一&#xff0c;其影响力不可忽视。本文将深入探讨Facebook的影响力&#xff0c;从多个角度揭示其在数字化社会中的引擎作用。 1. 社交互动的核心平台 Facebook作为社交…

使用Python递归重命名文件和文件夹

使用 Python 递归重命名文件和文件夹可以通过 os 模块和 os.path 模块来完成。下面是一个示例代码&#xff0c;演示如何递归地重命名文件和文件夹&#xff1a; 1、问题背景 在研究大型数字档案时&#xff0c;需要将这些档案复制到本地存储进行保存。这些档案通常存储在 USB 驱…

【机器学习】LoFTR:革命性图像特征批评技术等领跑者

LoFTR&#xff1a;革命性图像特征匹配技术的领跑者 一、引言二、LoFTR技术的创新之处三、LoFTR技术的实现原理四、LoFTR技术的代码实例五、结语 一、引言 在3D计算机视觉领域&#xff0c;图像特征匹配技术一直是研究的热点和难点。随着技术的不断发展&#xff0c;传统的特征检…

力扣:48. 旋转图像(Java)

目录 题目描述&#xff1a;输入&#xff1a;输出&#xff1a;代码实现&#xff1a; 题目描述&#xff1a; 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要 使…

图神经网络实战(10)——归纳学习

图神经网络实战&#xff08;10&#xff09;——归纳学习 0. 前言1. 转导学习与归纳学习2. 蛋白质相互作用数据集3. 构建 GraphSAGE 模型实现归纳学习小结系列链接 0. 前言 归纳学习 (Inductive learning) 通过基于已观测训练数据&#xff0c;建立一个通用模型&#xff0c;使模…

Maven:Maven基础

Maven apache旗下的一个开源项目,一款用于管理和构建java项目的工具 什么是Maven 一个项目管理和构建工具,基于项目对象模型(POM)的概念,通过一小段描述信息来管理项目的构建,报告和文档. Maven的作用 依赖管理 方便快捷的管理项目依赖的资源jar包,避免版本冲突问题 统一…

C++的数据结构(四):队列

在数据结构中&#xff0c;队列&#xff08;Queue&#xff09;是一种特殊的线性表&#xff0c;只允许在表的前端&#xff08;front&#xff09;进行删除操作&#xff0c;而在表的后端&#xff08;rear&#xff09;进行插入操作。队列中没有元素时&#xff0c;称为空队列。队列的…

小程序的小组件

进度的组件 文字换行过滤 以及 排序 简单易懂 只为了记录工作 <template><div><ProgressBar :progress"progress" /><button click"increaseProgress">增加进度</button><view class"goods-name">12…

电脑锁屏快捷键是哪个?1分钟弄懂锁屏设置!

“当我暂时不需要使用电脑时&#xff0c;想给电脑设置锁屏&#xff0c;有朋友知道电脑锁屏快捷键是哪个吗&#xff1f;” 随着信息技术的飞速发展&#xff0c;我们在日常生活中经常需要使用电脑。然而&#xff0c;当我们暂时离开电脑时&#xff0c;如何确保电脑信息安全&#x…

【解决】Android APK文件安装时 已包含数字签名相同APP问题

引言 在开发Android程序过程中&#xff0c;编译好的APK文件&#xff0c;安装至Android手机时&#xff0c;有时会报 包含数字签名相同的APP 然后无法安装的问题&#xff0c;这可能是之前安装过同签名的APP&#xff0c;但是如果不知道哪个是&#xff0c;无法有效卸载&#xff0c;…

图文详解:synchronized关键字 及其底层原理

目录 一.线程安全问题 二.synchronized关键字 ▐ synchronized图解 ▐ 可重入锁及图解 ▐ synchronized用于方法上 三.Java标准库中synchronized的使用 四.synchronized的底层实现原理 一.线程安全问题 线程安全是指在多线程环境下&#xff0c;对共享资源的访问不会导致…

详解循环队列——链表与数组双版本

前言&#xff1a;本节内容主要是讲解循环队列。 在本篇中会讲到两个版本——数组版本、链表版本。本篇内容适合正在学习数据结构队列章节或者已经学过队列但对循环队列感觉模糊的友友们 。 首先先来看一下什么是循环队列 什么是循环队列 因为是刚开始讲解&#xff0c; 所以我们…

【基础绘图】 10.饼图

效果图&#xff1a; 主要步骤&#xff1a; 1. 数据准备&#xff1a;自己赋值的随机数 2. 图像绘制&#xff1a;绘制饼图 详细代码&#xff1a;着急的直接拖到最后有完整代码 步骤一&#xff1a;导入库包及图片存储路径并设置中文字体为宋体&#xff0c;西文为新罗马&#…

totoriseSVN 常见问题

1. SVN 无法 clean up 上传时没有关闭 Excel&#xff0c;导致传入了一些临时文件&#xff08;文件名以$开头&#xff09;&#xff0c;关闭文件后临时文件自动删除&#xff0c;导致 SVN 版本错乱&#xff0c;使用 CleanUp 功能无效 更新时提示【Previous operation has not fin…

win7 phpstudy 多站点无法保存hosts的原因

1、先找到hosts文件位置 C:\Windows\System32\drivers\etc hosts文件不是txt的后缀&#xff0c;它是一个系统文件 2、如果不显示需要查找隐藏文件 组织-》文件夹和搜索选项-》查看-》取消隐藏文件夹的的√ 3、文件无法编辑 属性不要勾选只读

【SAP-FICO】SAP-FICO生产订单-结算规则配置路径(OKO7)

需求&#xff1a; 作为一个ABAPer&#xff0c;有接到一个狗屁倒灶的配置需求&#xff0c;要求如下&#xff0c;给生产订单的结算规则显示出来 图1&#xff1a;找一个生产订单&#xff0c;显示其结算规则 CO03→菜单栏-表头→结算规则 图2&#xff1a;查看该生产订单&#xff0c…

SMB/RPC协议分析之-命名/匿名管道pipe

在前面的文章中&#xff0c;介绍了SMB协议共享相关的内容&#xff0c;详见我的专栏《网络攻防协议实战分析》&#xff0c;连接这里。在SMB协议中往往需要连接到对应的远程管道&#xff0c;如果你经常接触到SMB协议&#xff0c;相信你对于lsass&#xff0c;svcctl等多种命名管道…