当前位置: 首页 > news >正文

简易的仿桌面文件夹上传(vue2)

      <el-tab-pane label="项目资料">
          <Information :projectId="form.id"></Information>
        </el-tab-pane>

Information/index.vue

<template>
  <div>
    <el-row :gutter="20">
      <!--文件夹目录数据-->
      <el-col :span="4" :xs="24">
        <div class="head-container">
          <el-input
            v-model="name"
            placeholder="请输入文件夹名称"
            clearable
            size="small"
            prefix-icon="el-icon-search"
            style="margin-bottom: 20px"
          />
        </div>
        <div class="head-container">
          <el-tree
            :data="folderOptions"
            :props="defaultProps"
            :expand-on-click-node="false"
            :filter-node-method="filterNode"
            ref="tree"
            node-key="id"
            default-expand-all
            highlight-current
            @node-click="handleNodeClick"
          />
        </div>
      </el-col>
      <el-col :span="20" :xs="24">
        <div class="top">
<!--          <el-button size="small" type="primary" plain @click="goBack" v-if="currentFolder">-->
<!--            <i class="el-icon-back" style="margin-right: 2px;" /> 返回-->
<!--          </el-button>-->
          <el-upload
            v-if="currentFolder.id != 0"
            multiple
            :action="uploadFileUrl"
            :file-list="fileList"
            :on-success="handleUploadSuccess"
            :show-file-list="false"
            :headers="headers"
            ref="fileUpload">
            <el-button slot="trigger" size="small"  type="success" plain><i class="el-icon-upload" style="margin-right: 2px;"/>上传文件</el-button>
          </el-upload>
          <el-button  v-if="currentFolder.id != 0" size="small" type="primary" plain  @click="createNewFolder"><i class="el-icon-plus" style="margin-right: 2px;" /> 新建文件夹</el-button>
          <el-button type="info" plain @click="queryData"><i class="el-icon-refresh-left" style="margin-right: 2px;"></i>主目录</el-button>
        </div>
        <div class="main">
          <ContextMenu
            :menu="['重命名', '删除']"
            :row="selectedRow"
            @action="handleContextMenuAction">
            <el-table
            ref="singleTable"
            :data="files"
            highlight-current-row
            style="width: 100%"
            @row-dblclick="handleRowClick"
            @row-contextmenu="handleRightClick">
            <el-table-column label="文件名"  >
              <template slot-scope="scope">
<!--                <svg-icon :icon-class="scope.row.type === 'folder' ? 'folder' : 'fileEmpty'" style="font-size: 26px" />-->
                <div v-if="scope.row.type === 'folder'">
                  <svg-icon :icon-class="'folder'" style="font-size: 26px" />
                  <span  style="margin-left: 10px">{{ scope.row.name }}</span>
                </div>
                <file-upload v-else :only-list="true" v-model="scope.row.fileUrl"></file-upload>
              </template>
            </el-table-column>
            <el-table-column property="createBy"  label="创建者" ></el-table-column>
            <el-table-column property="createTime"  label="创建时间"></el-table-column>
          </el-table>
          </ContextMenu>
        </div>
      </el-col>
    </el-row>


    <!--      新建文件夹对话框-->
      <el-dialog
        title="新建文件夹"
        :visible.sync="dialogVisible"
         width="30%"
        append-to-body>
        <el-form :model="form" ref="numberValidateForm" label-width="100px" class="demo-ruleForm">
          <el-form-item
            label="文件名称"
            prop="name"
            :rules="[{ required: true, message: '文件名称不能为空'}]"
          >
            <el-input v-model.number="form.name" autocomplete="off"></el-input>
          </el-form-item>
        </el-form>
        <span slot="footer" class="dialog-footer">
          <el-button @click="dialogVisible = false">取 消</el-button>
          <el-button type="primary" @click="submitFile">确 定</el-button>
        </span>
      </el-dialog>
    </div>
</template>

<script>
import { getToken } from "@/utils/auth";
import FileUpload  from "@/components/FileUpload/index.vue";
import { getFolderFile, addFolder, getFildsList, addFile, delFolder, delFolderFile, updateFolder } from "@/api/system/project";
import ContextMenu from "@/components/ContextMenu/index.vue";
export default {
  name: 'Information',
  components: {
    FileUpload,
    ContextMenu
  },
  props: {
    // 项目id
    projectId: {
      type: String,
      default: ""
    }
  },
  data() {
    return {
      currentFolder: {
        id: 0,
      }, // 当前所在文件夹的信息
      folderOptions: null, // 文件夹树选项
      name: null,   // 文件夹名称
      defaultProps: {
        children: "children",
        label: "label"
      },
      invoiceFile: null,
      uploadFileUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传文件服务器地址
      headers: {
        Authorization: "Bearer " + getToken(),
      },
      fileList: [],
      dialogVisible: false,
      form: {
        name: ''
      },
      files: [],
      contextMenuVisible: false, // 是否显示右键弹框
      menuPosition: { x: 0, y: 0 },  // 右键菜单的显示位置
      selectedRow: null // 右键选中的行
    };
  },
  watch: {
    // 根据名称筛选部门树
    name(val) {
      this.$refs.tree.filter(val);
    }
  },
  created() {
   this.queryData()
  },
  methods: {
    // 初始化列表
    queryData() {
      this.getDeptTree();
      this.getList(0);
    },
    // 获取列表数据
    getList(parentId) {
      getFolderFile({ parentId, projectId: this.projectId }).then(response => {
        let folders = response.data.map(item => ({
          ...item,
          type: 'folder'
        }));
        getFildsList(parentId, this.projectId).then(res => {
          let files = res.data.map(item => ({
            ...item,
            name: item.fileName,
            type: 'fileEmpty'
          }));
          this.files = [...folders, ...files];
        });
      });
    },
    // 文件表格点击事件
    handleRowClick(row) {
      if (row.type === 'folder') {
        this.currentFolder = row;
        this.getList(row.id);
        // 选中树形结构中的对应节点
        this.$nextTick(() => {
          this.$refs.tree.setCurrentKey(row.id); // 使用当前文件夹的 id 设置树节点选中
        });
      }
    },
    // 右键点击事件
    handleRightClick(row, column, event) {
      event.preventDefault();  // 阻止默认右键菜单
      this.selectedRow = row;  // 设置当前右键点击的行
      console.log(' this.selectedRow', this.selectedRow)
    },
    // 鼠标右键的点击事件
    handleContextMenuAction(action, row) {
      console.log('handleContextMenuAction', action, row)
      if (action === "删除") {
        this.deleteRow(row); // 删除操作
      } else if (action === "重命名") {
        this.renameRow(row); // 重命名操作
      }
    },
    // 删除
    deleteRow(row) {
      this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        if (row.type === 'folder') {
          delFolder(row.id).then(response => {
            this.getList(this.currentFolder.id);
          });
        } else {
          delFolderFile(row.id).then(response => {
            this.getList(this.currentFolder.id);
          });
        }
        this.$message({
          type: 'success',
          message: '删除成功!'
        });
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消删除'
        });
      });
    },
    // 重命名
    renameRow(row) {
      this.$prompt('', '重新命名', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        inputValue: row.name
      }).then(({ value }) => {
        if(row.type === 'folder') {
          updateFolder({id: row.id, name: value}).then(response => {
            this.getList(this.currentFolder.id);
            this.$message({
              type: 'success',
              message: '修改成功!'
            });
          })
        }
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '取消输入'
        });
      });
    },
    /** 查询部门下拉树结构 */
    getDeptTree() {
      getFolderFile({ projectId: this.projectId}).then(response => {
        let originData = response.data.map(item => {
          return {
            id: item.id,
            parentId: item.parentId,
            label: item.name,
            children: item.children
          }
        })
        this.folderOptions = this.handleTree(originData)
      });
    },
    // 筛选节点
    filterNode(value, data) {
      if (!value) return true;
      return data.label.indexOf(value) !== -1;
    },
    // 节点单击事件
    handleNodeClick(data) {
      this.currentFolder = data;
      this.getList(data.id);
    },
    // 文件上传成功的回调
    handleUploadSuccess(res, file) {
      if (res.code === 200) {

        const params = {
          folderId: this.currentFolder.id || 0,
          fileName: res.originalFilename,
          fileUrl: res.url,
          projectId: this.projectId
        }
        // 上传文件
        addFile(params).then(res => {
          this.getList(this.currentFolder.id)
          this.$modal.msgSuccess("上传成功");
        })
      } else {
        this.$modal.msgError(res.msg);
      }
    },
    // 新建文件夹
    createNewFolder() {
      this.dialogVisible = true;
    },
    // 提交新建文件夹
    submitFile() {
      // 在这里实现新建文件夹的逻辑
      const params = {
        parentId: this.currentFolder.id,
        name: this.form.name,
        projectId: this.projectId
      };
      addFolder(params).then(res => {
        this.getList(this.currentFolder.id)
        this.getDeptTree();
        this.form = {
          name: undefined
        }
      })
      this.dialogVisible = false;
    },
  }
};
</script>


<style  lang="scss">
.el-popover {
  z-index: 9999 !important;
}

::v-deep .svg-icon {
  vertical-align: -0.28em;
}

.top {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  //margin-bottom: 20px;
  height: 56px;
  .el-button {
    margin-right: 10px;
  }
}
.file-name {
  font-size: 14px;
  margin-left: 10px;
  width: 300px;
}
.file-creater {
  font-size: 14px;
  width: 100px;
}
.file-time {
  font-size: 14px;
  color: #999;
  width: 100px;
}
.file-desc {
  color: #999;
  font-size: 14px;
}
.main {
  border: 1px solid #eee;
  border-radius: 4px;
  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.1);
  margin: 0 10px;
  padding: 16px;
  box-sizing: border-box;
  .h-file-row, .h-file-title {
    display: flex;
    align-items: center;
    border-bottom: 1px solid #eee;
    padding: 6px 0;
  }
  .h-file-row:hover {
    background-color: #f0f9ff;
  }
}
</style>


ContentMenu/index.vue

<template>
  <div ref="contentRef" v-bind="$attrs">
    <slot></slot>
    <Teleport to="body" v-if="showMenu">
      <div  class="content-menu" :style="{ left: x + 'px', top: y + 'px' }">
        <div class="menu-list">
          <div
            class="menu-item"
            v-for="(item, index) in menu"
            :key="index"
            @click="handleMenuClick(item, $event)">
            {{ item }}
          </div>
        </div>
      </div>
    </Teleport>
  </div>
</template>

<script>
import Teleport from "../Teleport/index.vue"

export default {
  name: 'ContextMenu',
  components: {
    Teleport
  },
  props: {
    menu: {
      type: Array,
      default: () => ['重命名', '删除']
    },
    row: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      showMenu: false,
      x: 0,
      y: 0,
      mode: process.env.NODE_ENV,
      isTeleport: false, // 控制什么时候被传送
    };
  },
  mounted() {
    if (this.mode === 'production') {
      return;
    }
    const div = this.$refs.contentRef;
    div.addEventListener('contextmenu', this.showContextMenu);
    window.addEventListener('click', this.closeMenu, true);
  },
  beforeDestroy() {
    const div = this.$refs.contentRef;
    if (div) {
      div.removeEventListener('contextmenu', this.showContextMenu);
    }
    window.removeEventListener('click', this.closeMenu);
  },
  methods: {
    showContextMenu(e) {
      e.preventDefault();
      e.stopPropagation();

      this.x = e.clientX;
      this.y = e.pageY;
      this.showMenu = true;
    },
    closeMenu() {
      this.showMenu = false;
    },
    handleMenuClick(item, e) {
      e.stopPropagation();
      this.showMenu = false;
      this.$emit("action", item, this.row);
    }
  }
};
</script>

<style lang="scss" scoped>
.content-menu {
  position: absolute;
  cursor: pointer;
  z-index: 3001;
  width: 180px;
  background-color: #fff;
  padding: 8px 0;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}

.menu-list {
  text-align: center;
  letter-spacing: 4px;
  min-width: 100px;
}

.menu-item {
  padding: 6px 0;
  font-size: 12px;
  cursor: pointer;
  &:hover {
    background-color: #f6f6f6;
  }
}
</style>

Teleport/index.vue

<script>
export default {
  name: 'teleport',
  props: {
    /* 移动至哪个标签内,最好使用id */
    to: {
      type: String,
      required: true
    }
  },

  mounted() {
    // 把内容移动到指定的标签内this.to渲染
    document.querySelector(this.to).appendChild(this.$el)
  },

  beforeDestroy() {
    // 移除内容
    const targetElement = document.querySelector(this.to)
    if (targetElement && targetElement.contains(this.$el)) {
      targetElement.removeChild(this.$el)
    }
  },
  render() {
    return this.$scopedSlots.default()
  }
}
</script>

相关文章:

  • AI进展不止于基准:深度解析Grok 3的局限
  • 为AI聊天工具添加一个知识系统 之109 详细设计之50 三性三量三境
  • 数据分析--数据清洗
  • C++ 设计模式-外观模式
  • openharmony中HDF驱动框架关键流程说明-观察者模式
  • Redis7——基础篇(四)
  • 安卓鸿蒙应用开发架构变迁
  • HTML之JavaScript Form表单事件
  • [生活杂项][运动教程]自由泳
  • 【拥抱AI】GPT Researcher的诞生
  • qemu-kvm源码解析-cpu虚拟化
  • 基于SpringBoot+Vue的在线电影购票系统的设计与实现
  • Linux中进程的状态2
  • 【杂谈】加油!!!!
  • 根据研究主题自动生成研究报告,Open Deep Research远程服务器云部署
  • Medians
  • vscode通过ssh连接服务器实现免密登录+删除
  • 【DeepSeek三部曲】DeepSeek-R1论文详细解读
  • Python 基础-使用dict和set
  • Elon Musk的AI公司xAI重磅发布Grok 3,挑战OpenAI和Google
  • 新华时评:中国维护国际经贸秩序的立场坚定不移
  • 阚吉林任重庆市民政局党组书记,原任市委组织部主持日常工作的副部长
  • 甘肃省政府原副省长赵金云被决定逮捕
  • 中华人民共和国和俄罗斯联邦关于进一步加强合作维护国际法权威的联合声明
  • 98岁动物学家、北京大学教授杨安峰逝世
  • 上交现场配乐4K修复版《神女》:默片巅峰有了新的打开方式