Vue3实战uu云盘(三):文件删除+文件移动+目录导航+上传优化/文件过滤/搜索

一、文件删除

(1)选中了之后才可以删除,没有选中时就显示暗调删除按钮
(2)实现选中高亮功能
(3)单个删除
(4)批量删除

Main.vue中 

                <!-- 按钮3 -->
                <!-- 如果selectFileIdList数组的长度为0(即数组为空),则HTML元素的disabled属性将被设置为true,从而使该元素变为禁用状态 -->
                <el-button type="danger" @click="delFileBatch" :disabled="selectFileIdList.length == 0">
                    <span class="iconfont icon-del"></span>
                    批量删除
                </el-button>

.....

<span class="iconfont icon-del" @click="delFile(row)">
                                    删除
</span>

......

// 定义多选文件夹列表
const selectFileIdList = ref([]);
// 多选
const rowSelected = (rows) => {
    // 重置为一个空数组,清空数据
    selectFileIdList.value = [];
    // 遍历 rows 数组中的每一个元素
    rows.forEach((item) => {
        // 经遍历到的fileId添加到selectFileIdList中
        selectFileIdList.value.push(item.fileId);
    });
};
// 删除单个文件
const delFile = (row) => {
    // 调用Confirm
    proxy.Confirm(
        `你确定要删除【$row.fileName】吗?删除的文件可在 10 天内通过回收站还原`,
        async () => {
            let result = await proxy.Request({
                url: api.delFile,
                params: {
                    fileIds: row.fileId,
                },
            });
            if (!result) {
                return;
            }
            // 重新获取数据
            loadDataList();
        }
    );
};
// 批量删除文件
const delFileBatch = () => {
    // 如果要删除的长度为0就不执行
    if (selectFileIdList.value.length == 0) {
        return;
    }
    // 调用Confirm
    proxy.Confirm(
        // 第一个参数是一个字符串,用于显示给用户的确认消息
        `你确定要删除这些文件吗?删除的文件可在 10 天内通过回收站还原`,
        // 第二个参数,当用户点击确认按钮后,这个异步函数会被执行
        async () => {
            // 使用await关键字调用proxy.Request方法,发送一个HTTP请求到服务器
            // 接收proxy.Request方法的响应并将其存储在result变量中
            let result = await proxy.Request({
                // 请求的URL来自api.delFile,这可能是一个常量或配置对象中的属性,指向删除文件的API端点。
                url: api.delFile,
                // join将这个数组转换为一个由逗号分隔的字符串(因为批量删除多个ids所以要分开),作为查询参数发送的文件ID列表
                params: {
                    fileIds: selectFileIdList.value.join(","),
                },
            });
            // 处理响应,
            if (!result) {
                return;
            }
            // 重新获取数据
            loadDataList();
        }
    );
};

 二、文件移动

(1)定义全局组件,FolderSelect.vue

(因为文件移动到哪个文件夹,文件保存到哪个文件夹都要用到,所以封装一个全局组件)

别忘记在Main.js中引入

src/components/FolderSelect.vue(不全面,未添加导航)

<!-- 移动/保存到哪个文件夹组件 -->
<template>
    <div>
        <!-- 弹出的对话框组件Dialog -->
        <Dialog
          :show="dialogConfig.show"
          :title="dialogConfig.title"
          :buttons="dialogConfig.buttons"
          width="600px"
          :showCancel="true"
          @close="dialogConfig.show = false">
            <!-- 目录导航 -->
            <div class="navigation-panel"></div>
            <!-- 文件夹列表 -->
            <div class="folder-list" v-if="folderList.length > 0">
                <!-- 每一项文件夹 -->
                <div
                   class="folder-item"
                   v-for="item in folderList"
                   @click="selectFolder(item)">
                   <!-- 文件类型为0时即文件夹就显示文件夹的图标 -->
                   <Icon :fileType="0"></Icon>
                   <!-- 每个文件夹的名字 -->
                   <span class="file-name">{{ item.fileName }}</span>
               </div>
            </div>
            <!-- 判断 -->
            <div v-else class="tips">
                移动到 <span>{{ currentFolder.fileName }}</span> 文件夹
            </div>
        </Dialog>
    </div>
</template>

<script setup>
import { ref, reactive, getCurrentInstance } from "vue";
import { useRouter, useRoute } from "vue-router";

const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();

const api = {
    loadAllFolder: "/file/loadAllFolder",
};

// 定义弹出框的属性
const dialogConfig = ref({
    show: false,
    // 对话框的标题
    title: "移动到",
    buttons: [
        {
            type: "primary",
            text: "移动到此",// 按钮上的文字
            // 当按钮被点击时触发的回调函数。在这个例子中,点击按钮会调用 folderSelect() 函数。
            click: (e) => {
                folderSelect();
            },
        },
    ],
});

// 目录列表
const folderList = ref([]);
// 父级ID
const filePid = ref("0");
// 当前目录ID
const currentFileIds = ref([]);
// 当前文件夹
const currentFolder = ref({});

// 获取所有的目录文件夹列表
const loadAllFolder = async () => {
    // API请求
    // 使用 await 等待 proxy.Request 方法的执行结果存储到result里
    let result = await proxy.Request({
        // 指定请求的url
        url: api.loadAllFolder,
        // 传递父id和当前文件夹id
        params: {
            filePid: filePid.value,
            currentFileIds: currentFileIds.value,
        },
    });
    // 判断结果
    if (!result) {
        return;
    }
    folderList.value = result.data;
};

// 展示弹出框对外的方法
const showFolderDialog = (currentFolder) => {
    dialogConfig.value.show = true;
    // 更新当前文件id数组
    currentFileIds.value = currentFolder;
    // 在加载一次获取到的目录文件夹列表
    loadAllFolder();
};

// 关闭弹出框
const close = () => {
    dialogConfig.value.show = false;
};
// 向外暴露这两个函数,使得父组件Main可以调用这两个函数
defineExpose({ showFolderDialog, close });

// 选择目录(目录导航)
const selectFolder = (data) => {
    navigationRef.value.openFolder(data);
};

// 确定选择要移动到的目录
// 将选定的文件目录参数传递给父组件 Main 中的 folderSelect 函数
const emit = defineEmits(["folderSelect"]);
// 此方法回调在父组件中
const folderSelect = () => {
    emit("folderSelect", filePid.value);
};

</script>

<style lang="scss" scoped>
.navigation-panel {
    padding-left: 10px;
    background: #f1f1f1;
}

.folder-list {
    .folder-item {
        cursor: pointer;
        display: flex;
        align-items: center;
        padding: 10px;

        .file-name {
            display: inline-block;
            margin-left: 10px;
        }

        &:hover {
            background: #f8f8f8;
        }
    }

    max-height: calc(100vh - 200px);
    min-height: 200px;
}

.tips {
    text-align: center;
    line-height: 200px;

    span {
        color: #06a7ff;
    }
}</style>

(2)Main.vue中调用

(不全面,未添加导航栏)

<!-- 引入组件 -->
        <FolderSelect
          ref="folderSelectRef"
          @folderSelect="moveFolderDone"
        ></FolderSelect>
// 移动目录
const folderSelectRef = ref();
// 当前要移动的文件(单个文件)
const currentMoveFile = ref({});
// 移动单个文件
const moveFolder = (data) => {
    // 存储当前要移动的单个文件的信息
    currentMoveFile.value = data;
    // 把当前文件id给showFolderDialog方法
    folderSelectRef.value.showFolderDialog(currentFolder.value.fileId);
};
// 移动批量文件
const moveFolderBatch = () => {
    // 清空当前要移动的文件数据
    currentMoveFile.value = {};
    // 把当前的文件夹id给showFolderDialog
    folderSelectRef.value.showFolderDialog(currentFolder.value.fileId);
};

// 点击按钮之后,移动文件操作
const moveFolderDone = async (folderId) => {
    // 如果要移动到当前目录,提醒无需移动
    if (
        // 如果当前移动的文件父级id等于此时要移动到的文件夹id或者当前文件夹的id等于要移动到的文件夹id
        currentMoveFile.value.filePid == folderId ||
        currentFolder.value.fileId == folderId
    ) {
        // 就提示无需移动
        proxy.Message.warning("文件正在当前目录,无需移动");
        return;
    }
    // 定义一个数组存放要移动的文件或者文件夹信息
    let fileIdsArray = [];
    // 如果是单个文件移动
    if (currentMoveFile.value.fileId) {
        // 就把当前移动的文件id传给这个数组
        fileIdsArray.push(currentMoveFile.value.fileId);
    } else {
        // 如果是多个文件移动
        // concat 连接多个数组
        // selectFileIdList 是指批量选择时选择的文件ID
        filedIdsArray = filedIdsArray.concat(selectFileIdList.value);
    }
    // 发请求并将结果存储
    let result = await proxy.Request({
        // 请求的url
        url: api.changeFileFolder,
        // 携带的参数
        params: {
            // 将 fileIdsArray 数组中的所有元素转换为一个由逗号分隔的字符串,赋值给fileIds
            fileIds: fileIdsArray.join(","),
            // 把folderId传到父文件id里面
            filePid: folderId,
        },
    });
    if (!result) {
        return;
    }
    // 调用子组件暴露的close方法,实现当前弹出框页面的关闭
    folderSelectRef.value.close();
    // 更新当前文件列表
    loadDataList();
};

三、目录导航(难点)

(1)导航栏组件(全局)

src/components/Navigation.vue
template结构

js
1.设置点击目录事件 openFolder:
2.暴露此事件供父组件使用:defineExpose({ openFolder });
3.设置当前路径 setpath:当点击后目录改变,路径也随之改变
4.获取当前路径的目录 getNavigationFolder
5.doCallback
6.监听路由
7.初始化设置init
8.setCurrentFolder 设置当前目录,点击导航跳转到所点击的目录
9.返回上一级 backParent

src/components/Navigation.vue

<template>
  <!-- 导航 -->
  <div class="top-navigation">
    <!-- 返回上一级 -->
    <template v-if="folderList.length > 0">
      <span class="back link" @click="backParent">返回上一级</span>
      <!-- 竖线 -->
      <el-divider direction="vertical" />
    </template>
    <!-- 全部文件:外面粗体的不能点 -->
    <span v-if="folderList.length == 0" class="all-file">全部文件</span>
    <!-- 全部文件:能点的 -->
    <span
      class="link"
      v-if="folderList.length > 0"
      @click="setCurrentFolder(-1)"
      >全部文件
    </span>
    <!-- 遍历文件列表 -->
    <template v-for="(item, index) in folderList">
      <!-- 图标 -->
      <span class="iconfont icon-right"></span>
      <!-- 文件名字可以点击 -->
      <span
        class="link"
        v-if="index < folderList.length - 1"
        @click="setCurrentFolder(index)"
        >{{ item.fileName }}
      </span>
      <!-- 文件名字不可以点击 -->
      <span class="text" v-if="index == folderList.length - 1">
          {{item.fileName}}
      </span>
    </template>
  </div>
</template>

<script setup>
import { ref, reactive, getCurrentInstance, watch } from "vue";
import { useRouter, useRoute } from "vue-router";

const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();

// 定义父组件Main.vue传递过来的值
const props = defineProps({
  // 默认开启路由监听
  watchPath: {
    // 是否监听路由变化
    type: Boolean,
    default: true,
  },
  shareId: {
    type: String,
  },
  adminShow: {
    type: Boolean,
    default: false,
  },
});

const api = {
  // 首页 获取当前目录 获取列表 参数(path:完整路径)
  getFolderInfo: "/file/getFolderInfo",
  // 外部分享 获取目录信息 参数(shareId:分享id / path:完整路径)
  getFolderInfo4Share: "/showShare/getFolderInfo",
  // 管理员 获取当前目录 参数(path:完整路径)
  getFolderInfo4Admin: "/admin/getFolderInfo",
};

// 分类
const category = ref();
// 目录的集合
const folderList = ref([]);
// 当前目录
const currentFolder = ref({ fileId: "0" });

// 初始化
const init = () => {
  // 初始目录集合 设置为一个空数组
  folderList.value = [];
  // 初始当前目录设置
  currentFolder.value = { fileId: "0" };
  // 调用
  doCallback();
};

// 点击目录openFolder
// 父组件 Main/FolderSelect 中调用该方法,实现目录(文件及)的预览
const openFolder = (data) => {
  // 把data赋值给文件id和文件name
  const { fileId, fileName } = data;
  // 定义folder
  const folder = {
    fileName: fileName,
    fileId: fileId,
  };
  // 把folder push进目录集合
  folderList.value.push(folder);
  // 更新当前目录
  currentFolder.value = folder;
  // 设置当前路径,当点击后目录改变,路径也随之改变,调用setPath
  setPath();
};
defineExpose({ openFolder });

// 返回上一级
const backParent = () => {
  // 查找当前文件夹的索引
  let currentIndex = null;
  for (let i = 0; i < folderList.value.length; i++) {
    if (folderList.value[i].fileId == currentFolder.value.fileId) {
      currentIndex = i;
      break;
    }
  }
  // 设置当前文件夹为上一级目录
  setCurrentFolder(currentIndex - 1);
};

// 点击导航 设置当前目录(点击目录,跳转到所点击的目录)
const setCurrentFolder = (index) => {
  // 如果点的是全部文件
  if (index == -1) {
    // 初始化数组
    currentFolder.value = { fileId: "0" };
    folderList.value = [];
  } else {
    // 当前目录的值更新为目录集合数组为index的值
    currentFolder.value = folderList.value[index];
    // 删除从index+1开始的长度为目录集合数组的长度 的数组
    // 对于 splice(start, deleteCount, ...items) 方法:
    //  start(必需):开始更改数组的位置的索引。
    // deleteCount(必需):要删除的元素数量。如果设置为 0,则不会删除任何元素。
    // ...items(可选):要添加到数组中的新元素
    folderList.value.splice(index + 1, folderList.value.length);
  }
  setPath();
};

// 设置当前路径,当点击后目录改变,路径也随之改变
const setPath = () => {
  if (!props.watchPath) {
    // 设置不监听路由回调方法
    doCallback();
    return;
  }
  // 定义路径数组
  let pathArray = [];
  // 遍历目录集合的每一项
  folderList.value.forEach((item) => {
    // 把每一项的fileId push 到 路径数组里
    pathArray.push(item.fileId);
  });
  // 设置路由
  router.push({
    // 当前路径path
    path: route.path,
    // 参数
    query:
    // 如果pathArray长度为0,参数就为空,否则就把路径用/隔开加入到pathArray里面,更新path,添加到参数里面
      pathArray.length == 0
        ? ""
        : {
            path: pathArray.join("/"),
          },
  });
};

// 获取当前路径的目录
const getNavigationFolder = async (path) => {
  // 根据给定的 path 和一些其他属性(如 props.shareId 和 props.adminShow)来确定请求哪个 API,然后发送一个请求来获取该路径下的目录信息,并将获取到的目录信息存储在 folderList.value 中。
  let url = api.getFolderInfo;
  if (props.shareId) {
    url = api.getFolderInfo4Share;
  }
  if (props.adminShow) {
    url = api.getFolderInfo4Admin;
  }

  let result = await proxy.Request({
    url: url,
    showLoading: false,
    params: {
      path: path,
      shareId: props.shareId,
    },
  });
  if (!result) {
    return;
  }
  folderList.value = result.data;
};

// 回调 将当前的参数传递给父组件 Main
// 定义了一个名为 navChange 的事件。这允许子组件在需要时触发这个事件,并传递一些数据给父组件。
const emit = defineEmits(["navChange"]);
const doCallback = () => {
  emit("navChange", {
    categoryId: category.value,
    curFolder: currentFolder.value,
  });
};

// 监听路由
watch(
  () => route,
  // 它会在route的值变化时被调用。
  // newVal是route的新值,oldVal是route的旧值
  (newVal, oldVal) => {
    if (!props.watchPath) {
      return;
    }
    if (
      // 如果不在main路径里面就不用管
      newVal.path.indexOf("/main") === -1 &&
      newVal.path.indexOf("/settings/fileList") === -1 &&
      newVal.path.indexOf("/share") === -1
    ) {
      return;
    }
    // 把新携带的路径赋值给path,可以拿到?后面的一坨,query参数是?后面的一截
    const path = newVal.query.path;
    // params是route路由,category是在router里面定义的
    const categoryId = newVal.params.category;
    category.value = categoryId;
    if (path == undefined) {
      // 调用
      init();
    } else {
      // 调用
      getNavigationFolder(path);
      // 刷新的时候要把当前目录设置进来
      // 使用split("/")方法将path字符串分割为一个数组pathArray
      let pathArray = path.split("/");
      // fileId被赋值给currentFolder.value
      currentFolder.value = {
        // 它使用数组的最后一个元素(即path中的最后一个部分)作为fileId
        fileId: pathArray[pathArray.length - 1],
      };
      doCallback();
    }
  },
  { immediate: true, deep: true }
);
</script>

<style lang="scss" scoped>
.top-navigation {
  font-size: 13px;
  display: flex;
  align-items: center;
  line-height: 40px;
  .all-file {
    font-weight: bold;
  }
  .link {
    color: #06a7ff;
    cursor: pointer;
  }
  .icon-right {
    color: #06a7ff;
    padding: 0px 5px;
    font-size: 13px;
  }
}
</style>

(2)main.js中引入

import Navigation from '@/components/Navigation.vue';
app.component('Navigation', Navigation);

(3)Main.vue中使用组件

<!-- 导航 -->
<Navigation ref="navigationRef" @navChange="navChange"></Navigation>

Main.vue中绑定点击事件
 

<span @click="preview(row)">{{ row.fileName }}</span>
<span @click="preview(row)">{{ row.fileName }}</span>

preview回调,预览

// 预览
const previewRef = ref();
const preview = (data) => {
    // 如果是文件夹
    if (data.folderType == 1) {
        // 就调用Navigation组件中的openFolder方法,实现预览
        navigationRef.value.openFolder(data);
        return;
    }
    if (data.status != 2) {
        proxy.Message.warning("文件未完成转码,无法预览");
        return;
    }
    previewRef.value.showPreview(data, 0);
};

navChange回调

// 目录,展示目录
const navChange = (data) => {
    // 从传入的 data 对象中解构出 curFolder 和 categoryId 两个属性,并将它们的值分别赋给新定义的常量 curFolder 和 categoryId
    const { curFolder, categoryId } = data;
    // 将当前文件夹的值更新为传过来的文件夹
    currentFolder.value = curFolder;
    // 展示
    showLoading.value = true;
    // 更新category
    category.value = categoryId;
    loadDataList();
};

无文件上传时,Main.vue展示

<!-- 判断没有文件时 -->
        <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>

四、 上传优化(列表自动刷新)

(1)上传回调 

Main.vue中

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

Framework.vue中

<component @addFile="addFile" ref="routerViewRef" :is="Component"></component>
// 上传文件回调
// 上传文件后的刷新列表(调用Uploader子组件中的函数)
const routerViewRef = ref();
const uploadCallbackHandler = () => {
    nextTick(() => {
        // 它首先等待DOM更新完成(通过nextTick)
        // 然后重新加载一个组件(可能是router-view)
        routerViewRef.value.reload();
        // 并最后调用一个函数来获取空间使用情况。
        getUseSpace();
    });
};

五、文件选择(过滤)

组件:分类文件类型
src/js/CategoryInfo.js

export default {
    "all": {
        accept: "*"
    },
    "video": {
        accept: ".mp4,.avi,.rmvb,.mkv,.mov"
    },
    "music": {
        accept: ".mp3,.wav,.wma,.mp2,.flac,.midi,.ra,.ape,.aac,.cda"
    },
    "image": {
        accept: ".jpeg,.jpg,.png,.gif,.bmp,.dds,.psd,.pdt,.webp,.xmp,.svg,.tiff"
    },
    "doc": {
        accept: ".pdf,.doc,.docx,.xls,.xlsx,.txt"
    },
    "others": {
        accept: "*"
    },
}

Main.vue中上传按钮

:accept="fileAccept"

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

六、搜索功能实现

Main.vue中搜索输入框

 <!-- 搜索输入框 -->
                <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>


回调 

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

七、文件移动的目录导航

FolderSelect.vue中

<!-- 目录导航 -->
            <div class="navigation-panel">
                <Navigation
                    ref="navigationRef"
                    @navChange="navChange"
                    :watchPath="false"
                ></Navigation>
            </div>
// 绑定导航栏
const navigationRef = ref();
// 调用Navigation子组件中的navChange,使得参数传递给该组件
const navChange = (data) => {
    const { curFolder } = data;
    currentFolder.value = curFolder;
    filePid.value = curFolder.fileId;
    loadAllFolder();
};
// 选择目录(目录导航)
const selectFolder = (data) => {
    navigationRef.value.openFolder(data);
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值