一二三应用开发平台应用开发示例(9)——文件夹功能高代码改造

背景

前面使用平台低代码配置功能,把实体配置、库表和模式化的代码生成出来了,实际上是用平台帮忙开发人员把“体力活”给干了。接下来,就需要在此基础上进行个性化逻辑的开发。

文件夹操作

增加树节点图标

文件夹作为典型的树形数据,为了增强美观性和直观性,为树节点设置图标,且文件夹展开和折叠使用不同的图标,效果如下:
image.png
实现如下:

<template>
  <el-tree
    ref="tree"
    class="aside-tree"
    :data="treeData"
    node-key="id"
    :default-expanded-keys="cacheTreeExpandedKeys"
    @current-change="handleTreeSelectChange"
    @node-expand="handleNodeExpand"
    @node-collapse="handleNodeCollapse"
  >
    <template #default="{ node, data }">
      <span class="el-tree-node__label">
        <el-icon v-if="node.expanded"><FolderOpened /></el-icon>
        <el-icon v-else><Folder /></el-icon>
        {{ data.label }}
      </span>
    </template>
  </el-tree>
</template>

增加树节点排序

文件夹比较特殊,既不像业务数据可以按创建时间或修改时间倒序排列,也不像基础数据那样可以通过排序号属性来指定。最终采用的策略是通过文件夹的名字,因此在原来模板化生成的代码中,添加排序处理。
image.png
系统使用的时候,可以通过添加前缀的方式来实现有序展示,效果如下:
image.png

添加树节点右键菜单

为了便于操作,我们使用右键菜单的方式来对树节点进行新增、删除、移动等,实现效果如下:
image.png
实现使用element plus自身的菜单组件el-menu,菜单项是根据当前用户对选中树节点对应的文件夹所拥有的权限项动态加载的,当没有任何权限时,会显示“无可用菜单”。

菜单代码如下:

<!-- 文件夹树右键菜单 -->
  <el-menu
    v-show="folderContextMenu.visible"
    ref="folderContextMenu"
    :style="{
      width: '150px',
      left: folderContextMenu.left + 'px',
      top: folderContextMenu.top + 'px',
      position: 'fixed',
      cursor: 'pointer',
      'z-index': 9999
    }"
    popper-append-to-body
    @mouseleave="folderContextMenu.visible = false"
    @select="folderContextMenuSelect"
  >
    <el-menu-item
      v-if="documentPermissionList.includes($constant.CREATE_FOLDER)"
      :index="$constant.CREATE_FOLDER"
    >
      <el-icon><FolderAdd /></el-icon>
      <template #title>创建</template>
    </el-menu-item>

    <el-menu-item
      v-if="documentPermissionList.includes($constant.RENAME_FOLDER)"
      :index="$constant.RENAME_FOLDER"
    >
      <el-icon><Edit /></el-icon>
      <template #title>更名</template>
    </el-menu-item>
    <el-menu-item
      v-if="documentPermissionList.includes($constant.REMOVE_FOLDER)"
      :index="$constant.REMOVE_FOLDER"
    >
      <el-icon><FolderRemove /></el-icon>
      <template #title>删除</template>
    </el-menu-item>

    <el-menu-item
      v-if="documentPermissionList.includes($constant.COPY_FOLDER)"
      :index="$constant.COPY_FOLDER"
    >
      <el-icon><CopyDocument /></el-icon>
      <template #title>复制到…</template>
    </el-menu-item>
    <el-menu-item
      v-if="documentPermissionList.includes($constant.MOVE_FOLDER)"
      :index="$constant.MOVE_FOLDER"
    >
      <el-icon><Position /></el-icon>
      <template #title>移动到…</template>
    </el-menu-item>
    <el-menu-item
      v-if="documentPermissionList.includes($constant.GRANT_PERMISSION_BY_USER_GROUP)"
      :index="$constant.GRANT_PERMISSION_BY_USER_GROUP"
    >
      <el-icon><Setting /></el-icon>
      <template #title>按用户组授权</template>
    </el-menu-item>
    <el-menu-item
      v-if="documentPermissionList.includes($constant.GRANT_PERMISSION_BY_ORGANIZATION)"
      :index="$constant.GRANT_PERMISSION_BY_ORGANIZATION"
    >
      <el-icon><Setting /></el-icon>
      <template #title>按组织授权</template>
    </el-menu-item>
    <el-menu-item
      v-if="documentPermissionList.includes($constant.UPLOAD_DOCUMENT)"
      :index="$constant.UPLOAD_DOCUMENT"
    >
      <el-icon><Upload /></el-icon>
      <template #title>上传文档</template>
    </el-menu-item>
    <el-menu-item v-if="documentPermissionList.length === 0" :index="$constant.NO_MENU_AVAILABLE">
      <el-icon><Close /></el-icon>
      <template #title>无可用菜单</template>
    </el-menu-item>
  </el-menu>

将以上菜单挂载到文件夹树控件的右键菜单,是给树设置node-contextmenu事件,代码如下:

<el-tree
    ref="tree"
    class="aside-tree"
    :data="treeData"
    node-key="id"
    :default-expanded-keys="cacheTreeExpandedKeys"
    @node-contextmenu="folderContextMenuShow"
    @current-change="handleTreeSelectChange"
    @node-expand="handleNodeExpand"
    @node-collapse="handleNodeCollapse"
  >

右键菜单显示方法folderContextMenuShow会请求后端服务,根据当前文件夹标识,查询当前用户拥有的权限项清单,并会调用文件夹右键菜单位置的方法setFolderContextMenu,代码如下:

    // 文件夹树右键菜单显示
    folderContextMenuShow(mouseEvent, data) {
      // 设置右键点击的树节点为当前树的选中节点,后续操作如更名是取当前节点,不设置将会产生错位问题
      this.$refs.tree.setCurrentKey(data.id)
      this.parentId = data.id

      // 动态获取权限
      this.$api.edoc.folderPermission.getFolderPermissionForUser(data.id).then((res) => {
        this.setFolderContextMenu(res.data, mouseEvent)
      })
    },
    // 设置文件夹右键菜单位置
    setFolderContextMenu(data, mouseEvent) {
      this.documentPermissionList = data
      this.folderContextMenu.visible = true
      this.$nextTick(() => {
        this.folderContextMenu.left = mouseEvent.clientX - 10
        const menuHeight = this.$refs.folderContextMenu.$el.clientHeight
        const areaHeight = document.documentElement.clientHeight

        if (mouseEvent.clientY + menuHeight > areaHeight) {
          // 当鼠标点击的y坐标加上菜单高度超出区域高度时
          this.folderContextMenu.top = mouseEvent.clientY - menuHeight + 25
        } else {
          this.folderContextMenu.top = mouseEvent.clientY - 25
        }
      })
    },

右键菜单的显示与隐藏,是通过在data中定义的folderContextMenu对象的visible属性控制的,树节点右击时打开,菜单项点击或鼠标移出菜单范围关闭。

处理前端常量

树节点右键菜单动态展示权限项时,使用到了诸多常量,前面在新建前端功能模块的时候只是提到过,没展开说,这里回顾下。
前端功能模块下有个子目录constant,是用来存放本功能模块的常量的,如下图所示:
image.png
平台在src根目录下的constant来定义平台级公共常量(common),并聚合各业务模块的常量定义,如下图所示:
image.png
然后在前端项目的全局文件main.ts中挂载常量,如下所示:

import constant from '@/constant/index'


// 挂载全局变量,兼容2.0习惯写法
app.config.globalProperties.$constant = constant

这样平台中业务模块的vue页面,就可以通过类似this.$constant.UPLOAD_DOCUMENT的方式来使用了。

处理右键菜单项点击

通过给el-menu控件增加@select事件监听,来响应右键菜单项的点击操作。

<el-menu
    v-show="folderContextMenu.visible"
    ref="folderContextMenu"
    :style="{
      width: '150px',
      left: folderContextMenu.left + 'px',
      top: folderContextMenu.top + 'px',
      position: 'fixed',
      cursor: 'pointer',
      'z-index': 9999
    }"
    popper-append-to-body
    @mouseleave="folderContextMenu.visible = false"
    @select="folderContextMenuSelect"
  >

其中对应的事件方法folderContextMenuSelect,只是一个总调度,代码如下:

// 文件夹树右键菜单命令
    folderContextMenuSelect(command) {
      if (command === this.$constant.CREATE_FOLDER) {
        this.createFolder()
      } else if (command === this.$constant.RENAME_FOLDER) {
        this.renameFolder()
      } else if (command === this.$constant.REMOVE_FOLDER) {
        this.removeFolder()
      } else if (command === this.$constant.COPY_FOLDER) {
        this.copyFolder()
      } else if (command === this.$constant.MOVE_FOLDER) {
        this.moveFolder()
      } else if (command === this.$constant.GRANT_PERMISSION_BY_USER_GROUP) {
        this.grantPermissionByUserGroup()
      } else if (command === this.$constant.GRANT_PERMISSION_BY_ORGANIZATION) {
        this.grantPermissionByOrganization()
      } else if (command === this.$constant.UPLOAD_DOCUMENT) {
        this.uploadDocument()
      }
      // 隐藏右键菜单
      this.folderContextMenu.visible = false
    }

调整树视图

为了提升用户体验,我们在树上直接通过右键菜单的方式来进行增删改操作,跟平台内置的配置模板不同,因此需要进行人工调整(后续平台仍可以扩展一个基于树进行增删改的视图模板)。

创建

以创建文件夹为例,修改folder目录下的tree.vue,引入新增视图add.view

  <AddPage ref="addPage" @refresh="refresh" />

  import AddPage from './add.vue'

  components: {
    AddPage
  }

用户在树节点上点击右键来创建文件夹,这时候目的和意图是明确的,就是在当前树节点下新建文件夹,因此需要系统自动处理,不应该再让用户选择上级文件夹,因此将上级文件夹的可见性设置为false,或者干脆在新增视图里就不要配置上级属性。
这时候,需要通过参数将树节点,也就是文件夹的上级标识传到新增视图add.vue中。
在树节点上右键项点击,在弹出的菜单中点击“创建”时,调用如下方法:

    // 新建文件夹
    createFolder() {
       this.$refs.addPage.init({ id: this.parentId })
    }

对于自关联的实体,平台为自动为add视图生成初始化后的钩子方法,来接收和设置传来的参数。

afterInit(param) {
      this.entityData.parentId = param.id
}

运行效果如下:
image.png
点击保存后,发现左侧树没有显示新添加的节点,检查后,增加回调方法。

// 新建文件夹后回调
folderCreateCallback(data) {
    // 构造树节点
    const node = { id: data.id, label: data.name }
    // 添加到树
    this.$refs.tree.append(node, this.$refs.tree.getCurrentNode())
}

更名

由于文件夹只有一个核心属性,名称,因此修改操作实际就是更名操作。
与上面创建非常类似,基于平台生成的修改视图微调样式即可。
image.png

  // 更名文件夹后回调
  folderRenameCallback(data) {
    const node = this.$refs.tree.getCurrentNode()
    node.label = data.name
  }

删除

删除前确认,删除后刷新。

    // 移除文件夹
    removeFolder() {
      this.$confirm('此操作将删除该文件夹下所有内容, 是否继续?', '确认', {
        type: 'warning'
      })
        .then(() => {
          const node = this.$refs.tree.getCurrentNode()
          this.$api.edoc.folder.remove(node.id).then(() => {
            // 删除完成后从树上移除节点
            this.$refs.tree.remove(node)
            // 设置被删除节点的上级为当前节点
            this.$refs.tree.setCurrentKey(node.parentId)
            this.parentId = node.parentId
            this.load()
          })
        })
        .catch(() => {
          this.$message.info('已取消')
        })
    }

复制

复制操作,右键菜单点击对应的菜单项后,需要弹出对话框中选择目标文件夹。

<CopyFolder ref="copyFolder" @confirm="folderCopyCallback" />

import CopyFolder from './copy.vue'                                                              

// 复制文件夹
copyFolder() {
      this.$refs.copyFolder.show(this.$refs.tree.getCurrentKey())
}

选择文件夹的页面实现如下:

<template>
  <Dialog title="请选择目标文件夹" v-model="visible" width="500px">
    <el-input v-model="searchValue" placeholder="请输入关键词" style="margin-bottom: 10px" />
    <el-tag>当前选中:{{ currentName }}</el-tag>
    <el-tree
      ref="tree"
      :filter-node-method="filterNode"
      :default-expanded-keys="cacheTreeExpandedKeys"
      node-key="id"
      :data="treeData"
      highlight-current
      :expand-on-click-node="false"
      @current-change="treeNodeChange"
    >
      <template #default="{ node, data }">
        <span class="el-tree-node__label">
          <el-icon v-if="node.expanded"><FolderOpened /></el-icon>
          <el-icon v-else><Folder /></el-icon>
          {{ data.label }}
        </span>
      </template>
    </el-tree>

    <template #footer>
      <el-button type="primary" @click="confirm">确定</el-button>
      <el-button @click="close">关闭</el-button>
    </template>
  </Dialog>
</template>

<script>
import { Dialog } from '@/components/abc/Dialog'
export default {
  components: { Dialog },

  data() {
    return {
      visible: false,
      treeData: [],
      currentId: '',
      currentName: '',
      cacheTreeExpandedKeys: [],
      // 搜索值
      searchValue: ''
    }
  },
  watch: {
    searchValue(value) {
      this.$refs.tree.filter(value)
    }
  },
  methods: {
    show(id) {
      this.$api.edoc.folder.tree().then((res) => {
        this.treeData = res.data
        this.$api.edoc.folder.get(id).then((res) => {
          this.cacheTreeExpandedKeys.push(res.data.parentId)
          this.visible = true
        })
      })
    },
    treeNodeChange(data) {
      this.currentId = data.id
      this.currentName = data.label
    },
    close() {
      // 清空数据
      this.searchValue = ''
      this.currentId = ''
      this.currentName = ''
      this.visible = false
    },
    // 根据名称查询树节点
    filterNode(value, data) {
      if (!value) return true
      return data.label.indexOf(value) !== -1
    },
    confirm() {
      if (!this.currentId || this.currentId == '') {
        this.$message.warning('请选择目标文件夹')
        return
      }
      // 更新父组件绑定值
      this.$emit('confirm', this.currentId)
      this.close()
    }
  }
}
</script>

<style lang="scss"></style>

选中后出发confirm事件,在事件中执行不同的操作。


// 复制文件夹回调
folderCopyCallback(parentId) {
  this.$api.edoc.folder.copy(this.currentId, parentId).then(() => {
    this.load()
  })
}

移动

移动与复制非常类似,不同之处,在于多了一个选项,是否保留权限,若选择不保留,则系统清空已配置的权限。
image.png

按用户组授权

整体实现效果如下:
image.png
右键单击某个文件夹,在弹出菜单中选择“按用户组授权”项,打开上图的对话框,用户组以树的方式展现在左侧,文件夹相关的权限项以复选框列表的方式展现在右侧。

点击某个用户组,可以查看对于该文件夹已经赋予了哪些权限。更改权限项的勾选,可以变更权限,包括新增、修改、删除权限。

按组织授权

按组织授权与按用户组授权实现非常类似,左侧的用户组树更换为了组织机构树,仅附上一张整体效果图:
image.png

上传文档

参照平台业务支撑模块的附件管理功能实现,将文档视为附件,其依附的主实体视为文件夹,效果如下:
image.png
前端

    <AttachmentUploader
      entity-type="Document"
      :entity-id="folderId"
      module-code="edoc"
      :show-success-files="true"
    />

分块上传、合并、存储均能正常处理。

开发平台资料

平台名称:一二三应用开发平台
平台简介:企业级通用低代码应用开发平台,免费全开源可商用
设计资料:csdn专栏
开源地址:Gitee
开源协议:MIT

应用系统资料

应用名称:一二三文档管理系统
应用简介: 企事业单位一站式文档管理系统,让组织内文档管理有序,协作高效、安全可控
设计文档:csdn专栏
开源地址:Gitee
开源协议:MIT

如果您在阅读本文时获得了帮助或受到了启发,希望您能够喜欢并收藏这篇文章,为它点赞~
请在评论区与我分享您的想法和心得,一起交流学习,不断进步,遇见更加优秀的自己!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学海无涯,行者无疆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值