当前位置:   article > 正文

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

vue

一、文件列表

1.封装全局组件Table.vue

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

src/components/Table.vue

  1. <template>
  2. <!-- 表格 -->
  3. <div>
  4. <el-table
  5. ref="dataTable"
  6. :data="dataSource.list || []"
  7. :height="tableHeight"
  8. :stripe="options.stripe"
  9. :border="options.border"
  10. header-row-class-name="table-header-row"
  11. highlight-current-row
  12. @row-click="handleRowClick"
  13. @selection-change="handleSelectionChange"
  14. >
  15. <!-- selection 选择框 -->
  16. <el-table-column
  17. v-if="options.selectType && options.selectType == 'checkbox'"
  18. type="selection"
  19. width="50"
  20. align="center"
  21. ></el-table-column>
  22. <!-- 序号 -->
  23. <el-table-column
  24. v-if="options.showIndex"
  25. label="序号"
  26. type="index"
  27. width="60"
  28. align="center"
  29. ></el-table-column>
  30. <!-- 数据列 -->
  31. <template v-for="(column, index) in columns">
  32. <!-- 如果数据列中有插槽, 将其改造成插槽 -->
  33. <template v-if="column.scopedSlots">
  34. <el-table-column
  35. :key="index"
  36. :prop="column.prop"
  37. :label="column.label"
  38. :align="column.align || 'left'"
  39. :width="column.width"
  40. >
  41. <template #default="scope">
  42. <slot
  43. :name="column.scopedSlots"
  44. :index="scope.$index"
  45. :row="scope.row"
  46. >
  47. </slot>
  48. </template>
  49. </el-table-column>
  50. </template>
  51. <!-- 如果不是插槽,就正常操作 -->
  52. <template v-else>
  53. <el-table-column
  54. :key="index"
  55. :prop="column.prop"
  56. :label="column.label"
  57. :align="column.align || 'left'"
  58. :width="column.width"
  59. :fixed="column.fixed"
  60. >
  61. </el-table-column>
  62. </template>
  63. </template>
  64. </el-table>
  65. <!-- 分页 -->
  66. <!-- page-sizes 每页显示个数选择器的选项设置 -->
  67. <!-- page-size 每页显示条目个数 -->
  68. <!-- current-page 当前页数 -->
  69. <!-- layout 组件布局,子组件名用逗号分隔 -->
  70. <!-- size-change page-size 改变时触发 -->
  71. <!-- current-change current-page 改变时触发 -->
  72. <div class="pagination" v-if="showPagination">
  73. <el-pagination
  74. v-if="dataSource.totalCount"
  75. background
  76. :total="dataSource.totalCount"
  77. :page-sizes="[15, 30, 50, 100]"
  78. :page-size="dataSource.pageSize"
  79. :current-page.sync="dataSource.pageNo"
  80. :layout="layout"
  81. @size-change="handlePageSizeChange"
  82. @current-change="handlePageNoChange"
  83. style="text-align: right"
  84. ></el-pagination>
  85. </div>
  86. </div>
  87. </template>
  88. <script setup>
  89. import { ref, computed } from "vue";
  90. // 将选中的行传递给父组件Main
  91. const emit = defineEmits(["rowSelected", "rowClick"]);
  92. // 子组件接受父组件的值
  93. const props = defineProps({
  94. dataSource: Object,
  95. showPagination: {
  96. type: Boolean,
  97. default: true,
  98. },
  99. showPageSize: {
  100. type: Boolean,
  101. default: true,
  102. },
  103. options: {
  104. type: Object,
  105. default: {
  106. extHeight: 0,
  107. showIndex: false,
  108. },
  109. },
  110. columns: Array,
  111. fetch: Function, // 获取数据的函数
  112. initFetch: {
  113. type: Boolean,
  114. default: true,
  115. },
  116. });
  117. // 分页处布局
  118. const layout = computed(() => {
  119. return `total, ${
  120. props.showPageSize ? "sizes" : ""
  121. }, prev, pager, next, jumper`;
  122. });
  123. // 计算顶部高度
  124. //顶部 60 , 内容区域距离顶部 20, 内容上下内间距 15*2 分页区域高度 46
  125. const topHeight = 60 + 20 + 30 + 46;
  126. // 计算当前表格高度,实现页面内部滚动
  127. const tableHeight = ref(
  128. props.options.tableHeight
  129. ? props.options.tableHeight
  130. : window.innerHeight - topHeight - props.options.extHeight
  131. );
  132. const init = () => {
  133. if (props.initFetch && props.fetch) {
  134. // 获取数据
  135. props.fetch();
  136. }
  137. };
  138. init();
  139. const dataTable = ref();
  140. // 清除选中
  141. const clearSelection = () => {
  142. dataTable.value.clearSelection();
  143. };
  144. // 设置行选中
  145. const setCurrentRow = (rowKey, rowValue) => {
  146. let row = props.dataSource.list.find((item) => {
  147. return item[rowKey] === rowValue;
  148. });
  149. dataTable.value.setCurrentRow(row);
  150. };
  151. // 将父组件最新的行信息更新到子组件中
  152. // 将子组件暴露出去,否则无法调用
  153. defineExpose({ setCurrentRow, clearSelection });
  154. // 行点击
  155. const handleRowClick = (row) => {
  156. emit("rowClick", row);
  157. };
  158. // 行选中(多行)
  159. const handleSelectionChange = (row) => {
  160. emit("rowSelected", row);
  161. };
  162. // 切换每页大小
  163. const handlePageSizeChange = (size) => {
  164. props.dataSource.pageSize = size;
  165. props.dataSource.pageNo = 1;
  166. // 获取数据
  167. props.fetch();
  168. };
  169. // 切换页码
  170. const handlePageNoChange = (pageNo) => {
  171. props.dataSource.pageNo = pageNo;
  172. // 获取数据
  173. props.fetch();
  174. };
  175. </script>
  176. <style lang="scss" scoped>
  177. .pagination {
  178. padding-top: 10px;
  179. padding-right: 10px;
  180. }
  181. .el-pagination {
  182. justify-content: right;
  183. }
  184. :deep .el-table__cell {
  185. padding: 4px 0px;
  186. }
  187. </style>

2.封装全局组件Icon.vue

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

src/components/Icon.vue

  1. <template>
  2. <!-- 图标 -->
  3. <span :style="{ width: width + 'px', height: width + 'px' }" class="icon">
  4. <img :src="getImage()" :style="{ 'object-fit': fit }" />
  5. </span>
  6. </template>
  7. <script setup>
  8. import { ref, reactive, getCurrentInstance } from "vue";
  9. const { proxy } = getCurrentInstance();
  10. const props = defineProps({
  11. fileType: {
  12. type: Number,
  13. },
  14. iconName: {
  15. type: String,
  16. },
  17. cover: {
  18. type: String,
  19. },
  20. width: {
  21. type: Number,
  22. default: 32,
  23. },
  24. fit: {
  25. type: String,
  26. default: "cover",
  27. },
  28. });
  29. const fileTypeMap = {
  30. 0: { desc: "目录", icon: "folder" },
  31. 1: { desc: "视频", icon: "video" },
  32. 2: { desc: "音频", icon: "music" },
  33. 3: { desc: "图片", icon: "image" },
  34. 4: { desc: "exe", icon: "pdf" },
  35. 5: { desc: "doc", icon: "word" },
  36. 6: { desc: "excel", icon: "excel" },
  37. 7: { desc: "纯文本", icon: "txt" },
  38. 8: { desc: "程序", icon: "code" },
  39. 9: { desc: "压缩包", icon: "zip" },
  40. 10: { desc: "其他文件", icon: "others" },
  41. };
  42. const getImage = () => {
  43. // 当上传的不是本地文件,而是服务器上转码之后的图片或者视频
  44. if (props.cover) {
  45. return proxy.globalInfo.imageUrl + props.cover;
  46. }
  47. let icon = "unknow_icon";
  48. // 根据文件名判断图标
  49. if (props.iconName) {
  50. icon = props.iconName;
  51. } else {
  52. // 根据文件类型判断图标
  53. const iconMap = fileTypeMap[props.fileType];
  54. if (iconMap != undefined) {
  55. icon = iconMap["icon"];
  56. }
  57. }
  58. return new URL(`/src/assets/icon-image/${icon}.png`, import.meta.url).href;
  59. };
  60. </script>
  61. <style lang="scss" scoped>
  62. .icon {
  63. text-align: center;
  64. display: inline-block;
  65. border-radius: 3px;
  66. overflow: hidden;
  67. img {
  68. width: 100%;
  69. height: 100%;
  70. }
  71. }
  72. </style>

3.main.js引入全局组件

  1. import Table from '@/components/Table.vue'
  2. import Icon from '@/components/Icon.vue'
  3. app.component("Table",Table)
  4. app.component("Icon",Icon)

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

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

src/assets/file.list.scss

  1. .top {
  2. margin-top: 20px;
  3. .top-op {
  4. display: flex;
  5. align-items: center;
  6. .btn {
  7. margin-right: 10px;
  8. }
  9. .search-panel {
  10. margin-left: 10px;
  11. width: 300px;
  12. }
  13. .icon-refresh {
  14. cursor: pointer;
  15. margin-left: 10px;
  16. }
  17. .not-allow {
  18. background: #d2d2d2 !important;
  19. cursor: not-allowed;
  20. }
  21. }
  22. }
  23. .file-list {
  24. .file-item {
  25. display: flex;
  26. align-items: center;
  27. padding: 6px 0px;
  28. .file-name {
  29. margin-left: 8px;
  30. flex: 1;
  31. width: 0;
  32. overflow: hidden;
  33. // 当对象内文本溢出时显示省略标记(...)
  34. text-overflow: ellipsis;
  35. // 不换行 强行文本在同一行显示
  36. white-space: nowrap;
  37. span {
  38. cursor: pointer;
  39. &:hover {
  40. color: #06a7ff;
  41. }
  42. }
  43. .transfer-status {
  44. font-size: 13px;
  45. margin-left: 10px;
  46. color: #e6a23c;
  47. }
  48. .transfer-fail {
  49. color: #f75000;
  50. }
  51. }
  52. .edit-panel {
  53. flex: 1;
  54. width: 0;
  55. display: flex;
  56. align-items: center;
  57. margin: 0px 5px;
  58. .iconfont {
  59. margin-left: 10px;
  60. background: #0c95f7;
  61. color: #fff;
  62. padding: 3px 5px;
  63. border-radius: 5px;
  64. cursor: pointer;
  65. }
  66. .not-allow {
  67. cursor: not-allowed;
  68. background: #7cb1d7;
  69. color: #ddd;
  70. text-decoration: none;
  71. }
  72. }
  73. .op {
  74. width: 280px;
  75. margin-left: 15px;
  76. .iconfont {
  77. font-size: 13px;
  78. margin-left: 5px;
  79. color: #06a7ff;
  80. cursor: pointer;
  81. }
  82. .iconfont::before {
  83. margin-right: 1px;
  84. }
  85. }
  86. }
  87. }
  88. // justify-content 设置主轴上的子元素排列方式
  89. // align-content 设置侧轴上的子元素的排列方式(多行)
  90. .no-data {
  91. // vh就是当前屏幕可见高度的1%
  92. // height:100vh == height:100%;
  93. // calc(100vh - 150px)表示整个浏览器窗口高度减去150px的大小
  94. height: calc(100vh - 150px);
  95. display: flex;
  96. // align-items 设置侧轴上的子元素的排列方式(单行)
  97. align-items: center;
  98. // 设置主轴上的子元素排列方式
  99. justify-content: center;
  100. .no-data-inner {
  101. text-align: center;
  102. .tips {
  103. margin-top: 10px;
  104. }
  105. .op-list {
  106. margin-top: 20px;
  107. display: flex;
  108. justify-content: center;
  109. align-items: center;
  110. .op-item {
  111. cursor: pointer;
  112. width: 100px;
  113. height: 100px;
  114. margin: 0px 10px;
  115. padding: 5px 0px;
  116. background: rgb(241, 241, 241);
  117. }
  118. }
  119. }
  120. }

src/views/main/Main.vue引入

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


5.文件列表搭建

完整版Main.vue

src/views/main/Main.vue

  1. <template>
  2. <div>
  3. <div class="top">
  4. <!-- 头部按钮处 -->
  5. <div class="top-op">
  6. <div class="btn">
  7. <!-- show-file-list 是否显示已上传文件列表 -->
  8. <!-- with-credentials 支持发送 cookie 凭证信息 -->
  9. <!-- multiple 是否支持多选文件 -->
  10. <!-- http-request 覆盖默认的 Xhr 行为,允许自行实现上传文件的请求 -->
  11. <!-- accept 接受上传的文件类型 -->
  12. <el-upload
  13. :show-file-list="false"
  14. :with-credentials="true"
  15. :multiple="true"
  16. :http-request="addFile"
  17. :accept="fileAccept"
  18. >
  19. <el-button type="primary">
  20. <span class="iconfont icon-upload"></span>
  21. &nbsp上传
  22. </el-button>
  23. </el-upload>
  24. </div>
  25. <el-button type="success" @click="newFolder" v-if="category == 'all'">
  26. <span class="iconfont icon-folder-add"></span>
  27. &nbsp新建文件夹
  28. </el-button>
  29. <el-button
  30. @click="delFileBatch"
  31. type="danger"
  32. :disabled="selectFileIdList.length == 0"
  33. >
  34. <span class="iconfont icon-del"></span>
  35. &nbsp批量删除
  36. </el-button>
  37. <el-button
  38. @click="moveFolderBatch"
  39. type="warning"
  40. :disabled="selectFileIdList.length == 0"
  41. >
  42. <span class="iconfont icon-move"></span>
  43. &nbsp批量移动
  44. </el-button>
  45. <div class="search-panel">
  46. <el-input
  47. clearable
  48. placeholder="请输入文件名搜索"
  49. v-model="fileNameFuzzy"
  50. @keyup.enter="search"
  51. >
  52. <template #suffix>
  53. <i class="iconfont icon-search" @click="search"></i>
  54. </template>
  55. </el-input>
  56. </div>
  57. <div class="iconfont icon-refresh" @click="loadDataList"></div>
  58. </div>
  59. <!-- 导航 -->
  60. <Navigation ref="navigationRef" @navChange="navChange"></Navigation>
  61. </div>
  62. <!-- 文件列表 -->
  63. <div class="file-list" v-if="tableData.list && tableData.list.length > 0">
  64. <Table
  65. ref="dataTableRef"
  66. :columns="columns"
  67. :showPagination="true"
  68. :dataSource="tableData"
  69. :fetch="loadDataList"
  70. :initFetch="false"
  71. :options="tableOptions"
  72. @rowSelected="rowSelected"
  73. >
  74. <!-- 文件名 -->
  75. <template #fileName="{ index, row }">
  76. <!-- showOp(row) 当鼠标放在当前行时,分享下载等图标出现 -->
  77. <!-- cancelShowOp(row) 当鼠标离开当前行时,分享下载等图标消失 -->
  78. <div
  79. class="file-item"
  80. @mouseenter="showOp(row)"
  81. @mouseleave="cancelShowOp(row)"
  82. >
  83. <!-- 显示文件图标 -->
  84. <template
  85. v-if="(row.fileType == 3 || row.fileType == 1) && row.status == 2"
  86. >
  87. <!-- 如果文件类型是图片或者视频,且已经成功转码,则执行 Icon中的cover -->
  88. <Icon :cover="row.fileCover" :width="32"></Icon>
  89. </template>
  90. <template v-else>
  91. <!-- 如果文件夹类型是文件,则文件类型是该文件类型 -->
  92. <Icon v-if="row.folderType == 0" :fileType="row.fileType"></Icon>
  93. <!-- 如果文件夹类型是目录,则文件类型就是目录0 -->
  94. <Icon v-if="row.folderType == 1" :fileType="0"></Icon>
  95. </template>
  96. <!-- 显示文件名称 -->
  97. <!-- v-if="!row.showEdit" 如果该行文件没有编辑 -->
  98. <span class="file-name" v-if="!row.showEdit" :title="row.fileName">
  99. <span @click="preview(row)">{{ row.fileName }}</span>
  100. <span v-if="row.status == 0" class="transfer-status">转码中</span>
  101. <span v-if="row.status == 1" class="transfer-status transfer-fail"
  102. >转码失败</span
  103. >
  104. </span>
  105. <!-- 点击新建文件夹时显示行 -->
  106. <div class="edit-panel" v-if="row.showEdit">
  107. <el-input
  108. v-model.trim="row.fileNameReal"
  109. ref="editNameRef"
  110. :maxLength="190"
  111. @keyup.enter="saveNameEdit(index)"
  112. >
  113. <template #suffix>{{ row.fileSuffix }}</template>
  114. </el-input>
  115. <!-- 对号 确定 -->
  116. <span
  117. :class="[
  118. 'iconfont icon-right1',
  119. row.fileNameReal ? '' : 'not-allow',
  120. ]"
  121. @click="saveNameEdit(index)"
  122. ></span>
  123. <!-- 叉号 取消 -->
  124. <span
  125. class="iconfont icon-error"
  126. @click="cancelNameEdit(index)"
  127. ></span>
  128. </div>
  129. <!-- 当鼠标放在当前行时显示 -->
  130. <span class="op">
  131. <template v-if="row.showOp && row.fileId && row.status == 2">
  132. <span class="iconfont icon-share1" @click="share(row)">
  133. 分享
  134. </span>
  135. <!-- 只有当是文件夹时才可下载 -->
  136. <span
  137. class="iconfont icon-download"
  138. v-if="row.folderType == 0"
  139. @click="download(row)"
  140. >
  141. 下载
  142. </span>
  143. <span class="iconfont icon-del" @click="delFile(row)">
  144. 删除
  145. </span>
  146. <span class="iconfont icon-edit" @click="editFileName(index)">
  147. 重命名
  148. </span>
  149. <span class="iconfont icon-move" @click="moveFolder(row)">
  150. 移动
  151. </span>
  152. </template>
  153. </span>
  154. </div>
  155. </template>
  156. <!-- 文件大小 -->
  157. <template #fileSize="{ index, row }">
  158. <span v-if="row.fileSize">
  159. {{ proxy.Utils.size2Str(row.fileSize) }}</span
  160. >
  161. </template>
  162. </Table>
  163. </div>
  164. <div class="no-data" v-else>
  165. <div class="no-data-inner">
  166. <Icon iconName="no_data" :width="120" fit="fill"></Icon>
  167. <div class="tips">当前目录为空,上传你的第一个文件吧</div>
  168. <div class="op-list">
  169. <el-upload
  170. :show-file-list="false"
  171. :with-credentials="true"
  172. :multiple="true"
  173. :http-request="addFile"
  174. :accept="fileAccept"
  175. >
  176. <div class="op-item">
  177. <Icon iconName="file" :width="60"></Icon>
  178. <div>上传文件</div>
  179. </div>
  180. </el-upload>
  181. <div class="op-item" v-if="category == 'all'" @click="newFolder">
  182. <Icon iconName="folder" :width="60"></Icon>
  183. <div>新建目录</div>
  184. </div>
  185. </div>
  186. </div>
  187. </div>
  188. <FolderSelect
  189. ref="folderSelectRef"
  190. @folderSelect="moveFolderDone"
  191. ></FolderSelect>
  192. <!-- 预览 -->
  193. <Preview ref="previewRef"></Preview>
  194. <!-- 分享 -->
  195. <ShareFile ref="shareRef"></ShareFile>
  196. </div>
  197. </template>
  198. <script setup>
  199. import CategoryInfo from "@/js/CategoryInfo.js";
  200. import ShareFile from "./ShareFile.vue";
  201. import { ref, reactive, getCurrentInstance, nextTick, computed } from "vue";
  202. import { useRouter, useRoute } from "vue-router";
  203. const { proxy } = getCurrentInstance();
  204. const router = useRouter();
  205. const route = useRoute();
  206. // 实现上传文件的请求
  207. // 将Main子组件页面的数据传递给Framwork父组件
  208. const emit = defineEmits(["addFile"]);
  209. const addFile = async (fileData) => {
  210. emit("addFile", { file: fileData.file, filePid: currentFolder.value.fileId });
  211. };
  212. // 添加文件回调
  213. const reload = () => {
  214. showLoading.value = false;
  215. loadDataList();
  216. };
  217. defineExpose({ reload });
  218. const api = {
  219. loadDataList: "/file/loadDataList",
  220. rename: "/file/rename",
  221. newFoloder: "/file/newFoloder",
  222. getFolderInfo: "/file/getFolderInfo",
  223. delFile: "/file/delFile",
  224. changeFileFolder: "/file/changeFileFolder",
  225. createDownloadUrl: "/file/createDownloadUrl",
  226. download: "/api/file/download",
  227. };
  228. // 实现文件选择
  229. const fileAccept = computed(() => {
  230. const categoryItem = CategoryInfo[category.value];
  231. return categoryItem ? categoryItem.accept : "*";
  232. });
  233. // 列表头信息
  234. const columns = [
  235. {
  236. label: "文件名",
  237. prop: "fileName",
  238. scopedSlots: "fileName",
  239. },
  240. {
  241. label: "修改时间",
  242. prop: "lastUpdateTime",
  243. width: 200,
  244. },
  245. {
  246. label: "文件大小",
  247. prop: "fileSize",
  248. scopedSlots: "fileSize",
  249. width: 200,
  250. },
  251. ];
  252. // 搜索功能
  253. const search = () => {
  254. showLoading.value = true;
  255. loadDataList();
  256. };
  257. // 数据源
  258. const tableData = ref({});
  259. // 表格选项
  260. const tableOptions = {
  261. extHeight: 50,
  262. selectType: "checkbox",
  263. };
  264. // 文件名
  265. const fileNameFuzzy = ref();
  266. const showLoading = ref(true);
  267. // 分类
  268. const category = ref();
  269. // 当前文件夹
  270. const currentFolder = ref({ fileId: 0 });
  271. // 获得数据;
  272. const loadDataList = async () => {
  273. let params = {
  274. // 页码
  275. pageNo: tableData.value.pageNo,
  276. // 分页大小
  277. pageSize: tableData.value.pageSize,
  278. // 文件名(模糊)
  279. fileNameFuzzy: fileNameFuzzy.value,
  280. // 分类
  281. category: category.value,
  282. // 文件父id
  283. filePid: currentFolder.value.fileId,
  284. };
  285. if (params.category !== "all") {
  286. delete params.filePid;
  287. }
  288. let result = await proxy.Request({
  289. url: api.loadDataList,
  290. showLoading: showLoading,
  291. params,
  292. });
  293. if (!result) {
  294. return;
  295. }
  296. tableData.value = result.data;
  297. editing.value = false;
  298. };
  299. // 当鼠标放在当前行时,分享下载等图标出现
  300. const showOp = (row) => {
  301. // 关闭所有的显示
  302. tableData.value.list.forEach((element) => {
  303. element.showOp = false;
  304. });
  305. // 只开启当前显示
  306. row.showOp = true;
  307. };
  308. const cancelShowOp = (row) => {
  309. row.showOp = false;
  310. };
  311. // 编辑行(新建文件夹时编辑行)
  312. // 当前编辑行状态
  313. const editing = ref(false);
  314. // 新建文件夹行内填充的内容绑定
  315. const editNameRef = ref();
  316. // 新建文件夹
  317. const newFolder = () => {
  318. // 如果当前编辑行存在,则再次点击新建文件夹按钮时不起作用
  319. if (editing.value) {
  320. return;
  321. }
  322. // 让其他行都不允许编辑
  323. tableData.value.list.forEach((element) => {
  324. element.showEdit = false;
  325. });
  326. editing.value = true;
  327. tableData.value.list.unshift({
  328. showEdit: true,
  329. fileType: 0,
  330. fileId: "",
  331. filePid: currentFolder.value.fileId,
  332. });
  333. nextTick(() => {
  334. editNameRef.value.focus();
  335. });
  336. };
  337. // 取消新建文件夹操作
  338. const cancelNameEdit = (index) => {
  339. const fileData = tableData.value.list[index];
  340. // 如果存在这个文件的话,说明此处是重命名操作,那么可以直接将编辑行关闭
  341. if (fileData.fileId) {
  342. fileData.showEdit = false;
  343. } else {
  344. // 如果不存在的话,那么直接将此行删除
  345. tableData.value.list.splice(index, 1);
  346. }
  347. // 当前编辑行状态为:未编辑
  348. editing.value = false;
  349. };
  350. // 确定新建文件夹操作
  351. const saveNameEdit = async (index) => {
  352. const { fileId, filePid, fileNameReal } = tableData.value.list[index];
  353. if (fileNameReal == "" || fileNameReal.indexOf("/") != -1) {
  354. proxy.Message.warning("文件名不能为空且不能含有斜杠");
  355. return;
  356. }
  357. // 重命名
  358. let url = api.rename;
  359. if (fileId == "") {
  360. // 当文件ID不存在时,新建目录
  361. url = api.newFoloder;
  362. }
  363. let result = await proxy.Request({
  364. url: url,
  365. params: {
  366. fileId,
  367. filePid: filePid,
  368. fileName: fileNameReal,
  369. },
  370. });
  371. if (!result) {
  372. return;
  373. }
  374. tableData.value.list[index] = result.data;
  375. editing.value = false;
  376. };
  377. // 重命名 编辑文件名
  378. const editFileName = (index) => {
  379. // 如果现在有新建文件夹的编辑行,那么先将其删除,并且将序号减一
  380. if (tableData.value.list[0].fileId == "") {
  381. tableData.value.list.splice(0, 1);
  382. index = index - 1;
  383. }
  384. tableData.value.list.forEach((element) => {
  385. element.showEdit = false;
  386. });
  387. let cureentData = tableData.value.list[index];
  388. cureentData.showEdit = true;
  389. //编辑文件
  390. if (cureentData.folderType == 0) {
  391. cureentData.fileNameReal = cureentData.fileName.substring(
  392. 0,
  393. cureentData.fileName.indexOf(".")
  394. );
  395. cureentData.fileSuffix = cureentData.fileName.substring(
  396. cureentData.fileName.indexOf(".")
  397. );
  398. } else {
  399. cureentData.fileNameReal = cureentData.fileName;
  400. cureentData.fileSuffix = "";
  401. }
  402. // 当前编辑行状态为true
  403. editing.value = true;
  404. nextTick(() => {
  405. editNameRef.value.focus();
  406. });
  407. };
  408. // 行选中
  409. // 多选 批量选中
  410. const selectFileIdList = ref([]);
  411. const rowSelected = (rows) => {
  412. selectFileIdList.value = [];
  413. rows.forEach((item) => {
  414. selectFileIdList.value.push(item.fileId);
  415. });
  416. };
  417. // 删除单个文件
  418. const delFile = (row) => {
  419. proxy.Confirm(
  420. `你确定要删除【$row.fileName】吗?删除的文件可在 10 天内通过回收站还原`,
  421. async () => {
  422. let result = await proxy.Request({
  423. url: api.delFile,
  424. params: {
  425. fileIds: row.fileId,
  426. },
  427. });
  428. if (!result) {
  429. return;
  430. }
  431. // 重新获取数据
  432. loadDataList();
  433. }
  434. );
  435. };
  436. // 批量删除文件
  437. const delFileBatch = () => {
  438. if (selectFileIdList.value.length == 0) {
  439. return;
  440. }
  441. proxy.Confirm(
  442. `你确定要删除这些文件吗?删除的文件可在 10 天内通过回收站还原`,
  443. async () => {
  444. let result = await proxy.Request({
  445. url: api.delFile,
  446. params: {
  447. fileIds: selectFileIdList.value.join(","),
  448. },
  449. });
  450. if (!result) {
  451. return;
  452. }
  453. // 重新获取数据
  454. loadDataList();
  455. }
  456. );
  457. };
  458. // 移动目录
  459. const folderSelectRef = ref();
  460. // 当前要移动的文件(单个文件)
  461. const currentMoveFile = ref({});
  462. // 移动单个文件
  463. const moveFolder = (data) => {
  464. currentMoveFile.value = data;
  465. folderSelectRef.value.showFolderDialog(currentFolder.value.fileId);
  466. };
  467. // 移动批量文件
  468. const moveFolderBatch = () => {
  469. currentMoveFile.value = {};
  470. folderSelectRef.value.showFolderDialog(currentFolder.value.fileId);
  471. };
  472. // 移动文件操作
  473. const moveFolderDone = async (folderId) => {
  474. // 如果要移动到当前目录,提醒无需移动
  475. if (
  476. currentMoveFile.value.filePid == folderId ||
  477. currentFolder.value.fileId == folderId
  478. ) {
  479. proxy.Message.warning("文件正在当前目录,无需移动");
  480. return;
  481. }
  482. let filedIdsArray = [];
  483. // 如果是单个文件移动
  484. if (currentMoveFile.value.fileId) {
  485. filedIdsArray.push(currentMoveFile.value.fileId);
  486. } else {
  487. // 如果是多个文件移动
  488. // concat 连接多个数组
  489. // selectFileIdList 是指批量选择时选择的文件ID
  490. filedIdsArray = filedIdsArray.concat(selectFileIdList.value);
  491. }
  492. let result = await proxy.Request({
  493. url: api.changeFileFolder,
  494. params: {
  495. fileIds: filedIdsArray.join(","),
  496. filePid: folderId,
  497. },
  498. });
  499. if (!result) {
  500. return;
  501. }
  502. // 调用子组件暴露的close方法,实现当前弹出框页面的关闭
  503. folderSelectRef.value.close();
  504. // 更新当前文件列表
  505. loadDataList();
  506. };
  507. // 绑定导航栏
  508. const navigationRef = ref();
  509. // 预览
  510. const previewRef = ref();
  511. const preview = (data) => {
  512. // 如果是目录(文件夹)
  513. if (data.folderType == 1) {
  514. navigationRef.value.openFolder(data);
  515. return;
  516. }
  517. if (data.status != 2) {
  518. proxy.Message.warning("文件未完成转码,无法预览");
  519. return;
  520. }
  521. previewRef.value.showPreview(data, 0);
  522. };
  523. // 目录
  524. const navChange = (data) => {
  525. const { curFolder, categoryId } = data;
  526. currentFolder.value = curFolder;
  527. showLoading.value = true;
  528. category.value = categoryId;
  529. loadDataList();
  530. };
  531. // 下载文件
  532. const download = async (row) => {
  533. let result = await proxy.Request({
  534. url: api.createDownloadUrl + "/" + row.fileId,
  535. });
  536. if (!result) {
  537. return;
  538. }
  539. window.location.href = api.download + "/" + result.data;
  540. };
  541. // 分享文件
  542. // 利用ShareFile组件暴露出的show函数,实现将Main组件中的函数传递给ShareFile组件
  543. const shareRef = ref();
  544. const share = (row) => {
  545. shareRef.value.show(row);
  546. };
  547. </script>
  548. <style lang="scss" scoped>
  549. @import "@/assets/file.list.scss";
  550. </style>

二、功能实现

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

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


src/utils/Utils.js

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

main.js引入

  1. import Utils from './utils/Utils'
  2. app.config.globalProperties.Utils=Utils

2.新建目录功能

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

回调:

  1. // 编辑行(新建文件夹时编辑行)
  2. // 当前编辑行状态
  3. const editing = ref(false);
  4. // 新建文件夹行内填充的内容绑定
  5. const editNameRef = ref();
  6. // 新建文件夹
  7. const newFolder = () => {
  8. // 如果当前编辑行存在,则再次点击新建文件夹按钮时不起作用
  9. // 确保在编辑现有项目时,不能同时开始编辑新的项目。
  10. if (editing.value) {
  11. return;
  12. }
  13. // 让其他行都不允许编辑
  14. tableData.value.list.forEach((element) => {
  15. element.showEdit = false;
  16. });
  17. // 表示现在有一个项目正在被编辑
  18. editing.value = true;
  19. // 在列表顶部添加新文件夹:
  20. tableData.value.list.unshift({
  21. showEdit: true,
  22. fileType: 0,
  23. fileId: "",
  24. filePid: currentFolder.value.fileId,// 父文件夹的ID
  25. });
  26. // 在下一个“tick”中将焦点设置到某个输入框:
  27. nextTick(() => {
  28. editNameRef.value.focus();
  29. });
  30. };
  31. // 取消新建文件夹操作
  32. const cancelNameEdit = (index) => {
  33. const fileData = tableData.value.list[index];
  34. // 如果存在这个文件的话,说明此处是重命名操作,那么可以直接将编辑行关闭
  35. if (fileData.fileId) {
  36. fileData.showEdit = false;
  37. } else {
  38. // 如果不存在的话,那么直接将此行删除
  39. // 删除位于 index 位置的一个项目。删除后,数组的长度将减少1,并且所有高于 index 的元素都会向下移动一个位置。
  40. tableData.value.list.splice(index, 1);
  41. }
  42. // 当前编辑行状态为:未编辑
  43. editing.value = false;
  44. };
  45. // 确定新建文件夹操作
  46. const saveNameEdit = async (index) => {
  47. // 使用解构赋值从tableData.value.list数组中的指定索引位置获取fileId、filePid和fileNameReal。
  48. const { fileId, filePid, fileNameReal } = tableData.value.list[index];
  49. // 如果文件名fileNameReal为空或包含斜杠(/),则显示警告并退出函数。
  50. if (fileNameReal == "" || fileNameReal.indexOf("/") != -1) {
  51. proxy.Message.warning("文件名不能为空且不能含有斜杠");
  52. return;
  53. }
  54. // 如果fileId为空,表示这是新建目录而不是重命名,所以将请求的URL设置为api.newFoloder;否则,使用默认的api.rename来重命名现有文件或文件夹。
  55. // 重命名
  56. let url = api.rename;
  57. if (fileId == "") {
  58. // 当文件ID不存在时,新建目录
  59. url = api.newFoloder;
  60. }
  61. // 使用proxy.Request发送一个异步请求,该请求包含URL和要传递的参数(如fileId、filePid和fileName)。这里假设proxy.Request是一个返回Promise的函数,用于发送HTTP请求。
  62. let result = await proxy.Request({
  63. url: url,
  64. params: {
  65. fileId,
  66. filePid: filePid,
  67. fileName: fileNameReal,
  68. },
  69. });
  70. // 如果请求没有成功(例如,返回null或undefined),则直接退出函数。
  71. if (!result) {
  72. return;
  73. }
  74. // 如果请求成功,使用响应中的数据更新tableData.value.list数组中的相应项。
  75. tableData.value.list[index] = result.data;
  76. // 关闭编辑状态
  77. editing.value = false;
  78. };

3.文件重命名

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

回调:

  1. // 重命名 编辑文件名
  2. const editFileName = (index) => {
  3. // 如果现在有新建文件夹的编辑行
  4. if (tableData.value.list[0].fileId == "") {
  5. // 那么先将其删除
  6. tableData.value.list.splice(0, 1);
  7. // 并且将序号减一,否则重命名会出错顺序
  8. index = index - 1;
  9. }
  10. tableData.value.list.forEach((element) => {
  11. // 默认情况下所有行都不显示编辑状态。
  12. element.showEdit = false;
  13. });
  14. // 获取要编辑的行的数据(根据传入的index)
  15. let cureentData = tableData.value.list[index];
  16. // 表示该行现在处于编辑状态
  17. cureentData.showEdit = true;
  18. //编辑文件
  19. if (cureentData.folderType == 0) {
  20. // 如果folderType为0,表示这是一个文件(或不是文件夹)
  21. // 使用substring和indexOf方法从文件名中提取文件名(不带后缀)和文件后缀
  22. cureentData.fileNameReal = cureentData.fileName.substring(
  23. 0,
  24. cureentData.fileName.indexOf(".")
  25. );
  26. cureentData.fileSuffix = cureentData.fileName.substring(
  27. cureentData.fileName.indexOf(".")
  28. );
  29. // 如果不是文件
  30. } else {
  31. // 直接将文件名赋给fileNameReal
  32. cureentData.fileNameReal = cureentData.fileName;
  33. // 没有后缀
  34. cureentData.fileSuffix = "";
  35. }
  36. // 当前编辑行状态为true
  37. editing.value = true;
  38. nextTick(() => {
  39. editNameRef.value.focus();
  40. });
  41. };

三、文件上传功能

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

  1. <el-upload :show-file-list="false" :with-credentials="true" :multiple="true" :http-request="addFile"
  2. :accept="fileAccept">
  3. <el-button type="primary">
  4. <span class="iconfont icon-upload"></span>
  5. 上传
  6. </el-button>
  7. </el-upload>

(2)Main.vue中

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

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

(3)Framework.vue中

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

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

(4)回调

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

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

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

框架搭建:

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

  1. <template>
  2. <div class="uploader-panel">
  3. <!-- 上传标题 -->
  4. <div class="uploader-title">
  5. <span>上传任务</span>
  6. <span class="tips">(仅展示本次上传任务)</span>
  7. </div>
  8. <!-- 上传列表 -->
  9. <div class="file-list">
  10. <!-- 遍历上传的每一项 -->
  11. <div v-for="(item, index) in fileList" class="file-item">
  12. <!-- 上传的每一项 -->
  13. <div class="upload-panel">
  14. <!-- 文件名 -->
  15. <div class="file-name">{{ item.fileName }}</div>
  16. <!-- 上传进度条 -->
  17. <div class="progress">
  18. <!-- 当状态为 上传中/上传完成/秒传时显示 -->
  19. <!-- Element UI 的进度条组件,percentage 属性(进度条的百分比)绑定到 item.uploadProgress 这个数据上。 -->
  20. <el-progress :percentage="item.uploadProgress" v-if="item.status == STATUS.uploading.value ||
  21. item.status == STATUS.upload_seconds.value ||
  22. item.status == STATUS.upload_finish.value
  23. "></el-progress>
  24. </div>
  25. <!-- 下方上传状态:图标+描述 -->
  26. <div class="upload-status">
  27. <!-- 图标:✔/✖ -->
  28. <!-- 一个静态的 'iconfont' 和一个根据 item.status 从 STATUS 对象中获取的图标类名。 -->
  29. <span :class="['iconfont', 'icon-' + STATUS[item.status].icon]"
  30. :style="{ color: STATUS[item.status].color }">
  31. </span>
  32. <!-- 状态描述:上传中/上传完成/秒传/失败 -->
  33. <span
  34. class="status"
  35. :style="{ color: STATUS[item.status].color }"
  36. >{{
  37. item.status == "fail" ? item.errorMsg : STATUS[item.status].desc
  38. }}
  39. </span>
  40. <!-- 上传中的大小显示,传了多少,速度 -->
  41. <!-- v-if表示只会在文件上传过程中显示,123kb/200mb -->
  42. <span
  43. class="upload-info"
  44. v-if="item.status == STATUS.uploading.value">
  45. {{ proxy.Utils.size2Str(item.uploadSize) }}/{{
  46. proxy.Utils.size2Str(item.totalSize)
  47. }}
  48. </span>
  49. </div>
  50. </div>
  51. <!-- 后面的操作按钮 -->
  52. <div class="op">
  53. <!-- 显示 MD5解析 信息 -->
  54. <!-- 解析中,圆形进度条,只在文件或数据的初始化阶段显示,而不是在整个上传过程中 -->
  55. <el-progress
  56. type="circle"
  57. :width="50"
  58. :percentage="item.md5Progress"
  59. v-if="item.status == STATUS.init.value"
  60. ></el-progress>
  61. <!-- 按钮 -->
  62. <div class="op-btn">
  63. <!-- 如果是上传中,提供暂停和上传两个按钮 -->
  64. <span v-if="item.status == STATUS.uploading.value">
  65. <!-- 上传按钮 -->
  66. <Icon
  67. :width="28"
  68. class="btn-item"
  69. iconName="upload"
  70. v-if="item.pause"
  71. title="上传"
  72. @click="startUpload(item.uid)"
  73. ></Icon>
  74. <!-- 暂停按钮 -->
  75. <Icon
  76. :width="28"
  77. class="btn-item"
  78. iconName="pause"
  79. title="暂停"
  80. @click="pauseUpload(item.uid)"
  81. v-else
  82. ></Icon>
  83. </span>
  84. <!-- 在上传过程中,不是解析&上传完成&秒传的情况下,提供删除按钮(不想传了) -->
  85. <Icon
  86. :width="28"
  87. class="del btn-item"
  88. iconName="del"
  89. title="删除"
  90. v-if="item.status != STATUS.init.value &&
  91. item.status != STATUS.upload_finish.value &&
  92. item.status != STATUS.upload_seconds.value
  93. "
  94. @click="delUpload(item.uid, index)"
  95. ></Icon>
  96. <!-- 在是上传完成/秒传的情况下,提供清除按钮 -->
  97. <Icon
  98. :width="28"
  99. class="clean btn-item"
  100. iconName="clean"
  101. title="清除"
  102. v-if="item.status == STATUS.upload_finish.value ||
  103. item.status == STATUS.upload_seconds.value
  104. "
  105. @click="delUpload(item.uid, index)"
  106. ></Icon>
  107. </div>
  108. </div>
  109. </div>
  110. <!-- 当没有文件上传时的显示 -->
  111. <div v-if="fileList.length == 0">
  112. <NoData msg="暂无上传任务"></NoData>
  113. </div>
  114. </div>
  115. </div>
  116. </template>
  117. ......
  118. <style lang="scss" scoped>
  119. .uploader-panel {
  120. .uploader-title {
  121. border-bottom: 1px solid #ddd;
  122. line-height: 40px;
  123. padding: 0px 10px;
  124. font-size: 15px;
  125. .tips {
  126. font-size: 13px;
  127. color: rgb(169, 169, 169);
  128. }
  129. }
  130. .file-list {
  131. // 如果内容溢出,则浏览器提供滚动条。
  132. overflow: auto;
  133. padding: 10px 0px;
  134. min-height: calc(100vh / 2);
  135. max-height: calc(100vh - 120px);
  136. .file-item {
  137. position: relative;
  138. display: flex;
  139. justify-content: center;
  140. align-items: center;
  141. padding: 3px 10px;
  142. background-color: #fff;
  143. border-bottom: 1px solid #ddd;
  144. }
  145. .file-item:nth-child(even) {
  146. background-color: #fcf8f4;
  147. }
  148. .upload-panel {
  149. flex: 1;
  150. .file-name {
  151. color: rgb(64, 62, 62);
  152. }
  153. .upload-status {
  154. display: flex;
  155. align-items: center;
  156. margin-top: 5px;
  157. .iconfont {
  158. margin-right: 3px;
  159. }
  160. .status {
  161. color: red;
  162. font-size: 13px;
  163. }
  164. .upload-info {
  165. margin-left: 5px;
  166. font-size: 12px;
  167. color: rgb(112, 111, 111);
  168. }
  169. }
  170. .progress {
  171. height: 10px;
  172. }
  173. }
  174. .op {
  175. width: 100px;
  176. display: flex;
  177. align-items: center;
  178. justify-content: flex-end;
  179. .op-btn {
  180. .btn-item {
  181. cursor: pointer;
  182. }
  183. .del,
  184. .clean {
  185. margin-left: 5px;
  186. }
  187. }
  188. }
  189. }
  190. }</style>

功能实现:

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

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

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

 

  1. <script setup>
  2. import {
  3. getCurrentInstance,
  4. onMounted,
  5. reactive,
  6. ref,
  7. watch,
  8. nextTick,
  9. } from "vue";
  10. import SparkMD5 from "spark-md5";
  11. const { proxy } = getCurrentInstance();
  12. const api = {
  13. upload: "/file/uploadFile",
  14. };
  15. // 定义不同的上传状态
  16. const STATUS = {
  17. emptyfile: {
  18. value: "emptyfile",
  19. desc: "文件为空",
  20. color: "#F75000",
  21. icon: "close",
  22. },
  23. fail: {
  24. value: "fail",
  25. desc: "上传失败",
  26. color: "#F75000",
  27. icon: "close",
  28. },
  29. init: {
  30. value: "init",
  31. desc: "解析中",
  32. color: "#e6a23c",
  33. icon: "clock",
  34. },
  35. uploading: {
  36. value: "uploading",
  37. desc: "上传中",
  38. color: "#409eff",
  39. icon: "upload",
  40. },
  41. upload_finish: {
  42. value: "upload_finish",
  43. desc: "上传完成",
  44. color: "#67c23a",
  45. icon: "ok",
  46. },
  47. upload_seconds: {
  48. value: "upload_seconds",
  49. desc: "秒传",
  50. color: "#67c23a",
  51. icon: "ok",
  52. },
  53. };
  54. // 分片时,每片的大小
  55. const chunkSize = 1024 * 1024 * 5;
  56. // 文件列表
  57. const fileList = ref([]);
  58. // 删除的文件的ID
  59. const delList = ref([]);
  60. const addFile = async (file, filePid) => {
  61. const fileItem = {
  62. // 文件
  63. file: file,
  64. // 文件ID
  65. uid: file.uid,
  66. // md5进度(转圈进度)
  67. md5Progress: 0,
  68. // md5值
  69. md5: null,
  70. // 文件名,文件展示的名字
  71. fileName: file.name,
  72. // 上传状态
  73. status: STATUS.init.value,
  74. // 已上传大小
  75. uploadSize: 0,
  76. // 文件总大小
  77. totalSize: file.size,
  78. // 上传进度
  79. uploadProgress: 0,
  80. //暂停
  81. pause: false,
  82. // 当前分片
  83. chunkIndex: 0,
  84. // 父级ID
  85. filePid: filePid,
  86. // 错误信息
  87. errorMsg: null,
  88. };
  89. // 把上传文件加到上传列表前面
  90. fileList.value.unshift(fileItem);
  91. // 如果文件大小为0,
  92. if (fileItem.totalSize == 0) {
  93. // 表示为空文件状态
  94. fileItem.status = STATUS.emptyfile.value;
  95. // 退出
  96. return;
  97. }
  98. // 文件大小不为0,代码将尝试计算该文件的MD5值
  99. let md5FileUid = await computeMD5(fileItem);
  100. // 检测md5值是否有效
  101. if (md5FileUid == null) {
  102. return;
  103. }
  104. // 上传文件
  105. uploadFile(md5FileUid);
  106. };
  107. // 暴露给父组件 FrameWork,方便其调用该方法
  108. defineExpose({ addFile });
  109. // 上传文件
  110. const emit = defineEmits(["uploadCallback"]);
  111. // 异步函数,接收两个参数:uid(文件的唯一标识符)和 chunkIndex(要上传的切片的索引,默认为0)
  112. const uploadFile = async (uid, chunkIndex) => {
  113. chunkIndex = chunkIndex ? chunkIndex : 0;
  114. // 获取当前文件
  115. let currentFile = getFileByUid(uid);
  116. // 计算切片数量
  117. const file = currentFile.file;
  118. const fileSize = currentFile.totalSize;
  119. const chunks = Math.ceil(fileSize / chunkSize);
  120. // 给定的 chunkIndex 开始,遍历所有切片
  121. for (let i = chunkIndex; i < chunks; i++) {
  122. // 判断如果在文件上传的过程中删除了文件,那么直接跳出循环
  123. // 调用 indexOf 方法来查找 uid 在 delList.value 列表中的索引
  124. let delIndex = delList.value.indexOf(uid);
  125. if (delIndex != -1) {
  126. // 使用 splice 方法来移除它。splice 方法接受两个参数:要开始移除的元素的索引(这里是 delIndex),以及要移除的元素数量(这里是 1,因为我们只移除一个元素)。
  127. delList.value.splice(delIndex, 1);
  128. break;
  129. }
  130. // 如果当前文件被暂停,那么直接跳出循环
  131. if (currentFile.pause) break;
  132. // 获取分片
  133. // start 变量表示当前数据块在原始文件中的起始字节位置
  134. let start = i * chunkSize;
  135. // 如果起始位置加上chunkSize超过了文件的总大小(fileSize),那么结束位置就是文件的总大小;否则,结束位置就是起始位置加上chunkSize
  136. let end = start + chunkSize >= fileSize ? fileSize : start + chunkSize;
  137. // 提取从start到end(不包括end)的字节范围,并返回一个新的Blob对象,该对象包含该范围内的数据。这个新的Blob对象(chunkFile)就是我们要上传的数据块。
  138. let chunkFile = file.slice(start, end);
  139. // 发起HTTP请求
  140. // uploadResult存储上传请求的响应结果
  141. let uploadResult = await proxy.Request({
  142. url: api.upload,//API的上传端点(URL)
  143. showLoading: false,
  144. dataType: "file",
  145. params: {
  146. file: chunkFile,//要上传的文件分块,它是一个Blob对象
  147. fileName: file.name,
  148. fileMd5: currentFile.md5,
  149. chunkIndex: i,
  150. chunks: chunks,//被分割的总片数
  151. fileId: currentFile.fileId,
  152. filePid: currentFile.filePid,
  153. },
  154. showError: false,
  155. // 报错
  156. // 它接收一个errorMsg参数,表示错误信息
  157. errorCallback: (errorMsg) => {
  158. // 然后,它将currentFile.status设置为失败状态
  159. currentFile.status = STATUS.fail.value;
  160. // 并将errorMsg保存到currentFile.errorMsg中
  161. currentFile.errorMsg = errorMsg;
  162. },
  163. // 进度更新
  164. // 接收一个event对象,该对象包含了关于上传进度的信息
  165. uploadProgressCallback: (event) => {
  166. // 从event中获取已加载的字节数loaded
  167. let loaded = event.loaded;
  168. if (loaded > fileSize) {
  169. // 检查已加载的字节数是否超过了文件总大小(fileSize),如果是,则将其设置为fileSize。
  170. loaded = fileSize;
  171. }
  172. // 更新currentFile.uploadSize为当前分块的起始位置加上已加载的字节数
  173. currentFile.uploadSize = i * chunkSize + loaded;
  174. // 计算上传进度百分比,并更新currentFile.uploadProgress
  175. currentFile.uploadProgress = Math.floor(
  176. (currentFile.uploadSize / fileSize) * 100
  177. );
  178. },
  179. });
  180. // 上传请求可能没有成功执行或返回了无效的结果
  181. if (uploadResult == null) {
  182. break;
  183. }
  184. // 更新文件信息
  185. currentFile.fileId = uploadResult.data.fileId;
  186. currentFile.status = STATUS[uploadResult.data.status].value;
  187. currentFile.chunkIndex = i;
  188. // 如果状态是秒传和上传完成,则执行以下操作
  189. if (
  190. uploadResult.data.status == STATUS.upload_seconds.value ||
  191. uploadResult.data.status == STATUS.upload_finish.value
  192. ) {
  193. // 上传进度条为100
  194. currentFile.uploadProgress = 100;
  195. // 上传结束后,uploaderCallback,将Framework中的列表刷新
  196. emit("uploadCallback");
  197. break;
  198. }
  199. }
  200. };
  201. // 计算文件的 MD5 值
  202. // 会对大文件进行分片处理
  203. const computeMD5 = (fileItem) => {
  204. let file = fileItem.file;
  205. // slice 分割文件
  206. // mozSlice 兼容firefox
  207. // webkitSlice 兼容webkit
  208. let blobSlice =
  209. File.prototype.slice ||
  210. File.prototype.mozSlice ||
  211. File.prototype.webkitSlice;
  212. // chunkSize 每片的大小
  213. // chunks 切片数量(向上取整)
  214. let chunks = Math.ceil(file.size / chunkSize);
  215. // 当前切片的下标为0
  216. let currentChunk = 0;
  217. // 创建SparkMD5的实例,计算MD5
  218. let spark = new SparkMD5.ArrayBuffer();
  219. // 使用 FileReader 读取文件的数据
  220. let fileReader = new FileReader();
  221. // 已删除文件的索引
  222. const delList = ref([]);
  223. // 加载数据
  224. // loadNext读取文件的下一个块
  225. let loadNext = () => {
  226. // 当前片段在文件中的起始字节位置
  227. let start = currentChunk * chunkSize;
  228. // 起始位置加上片段大小,超出文件的总大小(file.size),大小为file.size,不超出则将结束位置设置为文件的总大小。
  229. let end = start + chunkSize >= file.size ? file.size : start + chunkSize;
  230. // 来异步读取文件的指定片段
  231. fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
  232. };
  233. // 当 computeMD5 函数被调用时,它会立即开始读取文件的第一个片段。
  234. loadNext();
  235. // 使用 Promise 封装文件分片读取和 MD5 哈希值计算
  236. // 这个 Promise 将在文件的所有分片都被读取并计算完 MD5 哈希值后解决(resolve),并返回文件的唯一标识符(UID)
  237. return new Promise((resolve, reject) => {
  238. // 根据文件ID获取到文件
  239. let resultFile = getFileByUid(file.uid);
  240. // 当读取操作成功完成时调用
  241. // 当 FileReader 读取完一个文件分片后,会触发 onload 事件
  242. fileReader.onload = (e) => {
  243. // 向SparkMD5实例中添加数据
  244. spark.append(e.target.result); // Append array buffer
  245. // 切片下标+1
  246. currentChunk++;
  247. // 如果 currentChunk 小于 chunks(总分片数),则继续读取下一个分片
  248. // 自动分片解析
  249. if (currentChunk < chunks) {
  250. /* console.log(
  251. `第${file.name},${currentChunk}分片解析完成, 开始第${
  252. currentChunk + 1
  253. } / ${chunks}分片解析`
  254. ); */
  255. // 计算当前进度百分比,并更新 resultFile.md5Progress
  256. let percent = Math.floor((currentChunk / chunks) * 100);
  257. resultFile.md5Progress = percent;
  258. // 再次读取数据,读取下一个文件分片
  259. loadNext();
  260. } else {
  261. // 如果当前切片下标不比切片数量小,说明解析到最后了
  262. // 调用 SparkMD5 的 end() 方法来计算最终的 MD5 哈希值
  263. let md5 = spark.end();
  264. /* console.log(
  265. `MD5计算完成:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${
  266. file.size
  267. } 用时:${new Date().getTime() - time} ms`
  268. ); */
  269. // 释放 SparkMD5 实例占用的资源
  270. spark.destroy(); //释放缓存
  271. // 设置 resultFile.md5Progress 为 100,表示进度完成。
  272. resultFile.md5Progress = 100;
  273. // 设置 resultFile.status 为上传状态
  274. resultFile.status = STATUS.uploading.value;
  275. // 设置 resultFile.md5 为计算得到的 MD5 哈希值
  276. resultFile.md5 = md5;
  277. // 调用 resolve(fileItem.uid); 来解决 Promise,并返回文件的 UID
  278. resolve(fileItem.uid);
  279. }
  280. };
  281. // 当读取操作发生错误时调用
  282. fileReader.onerror = () => {
  283. // 将 resultFile 对象的 md5Progress 属性设置为 -1,表示 MD5 计算过程遇到了错误。
  284. resultFile.md5Progress = -1;
  285. // 设置文件状态为失败
  286. resultFile.status = STATUS.fail.value;
  287. resolve(fileItem.uid);
  288. };
  289. // Promise 链的一个捕获处理器(catch handler),用于处理 Promise 链中任何地方的错误
  290. }).catch((error) => {
  291. return null;
  292. });
  293. };
  294. // 根据文件ID获取到文件
  295. const getFileByUid = (uid) => {
  296. let file = fileList.value.find((item) => {
  297. return item.file.uid === uid;
  298. });
  299. return file;
  300. };
  301. </script>

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

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

引入

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

回调:

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

效果:

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/583942
推荐阅读
相关标签