可媲美Element Plus Tree组件研发之保存节点变更

咱们的JuanTree组件功能开发完基本的增删改操作,接下来就是保存操作了。为了方便组件的使用者,我们将内置dirtyData的封装,这样用户对接后台服务提交Tree数据的变更将变得非常简单。

这种封装不但简化了组件用户的开发工作,而且提升了产品的用户体验,组件状态不用做任何的刷新,各种操作对用户都是无感知的噢~

在这里插入图片描述

用法

只需要获取组件的引用调saveDirty方法即可:

在这里插入图片描述

用法非常简单!!

在这里插入图片描述

dirtyData中封装好了增删改的数据,注意!!这里我们暂时只考虑改节点标签名,真正将我们的JuanTree投入项目使用,往往我们要维护更多的后台模型相关的元数据,可以在此基础上很方便的进行扩展噢~

用户可以从dirtyData中分拣数据包装与后台接口交互的入参然后调取后台接口服务,后台提交成功后,再在ajax回调中调用done(...)以完成tree脏数据的flush完成同步。这个过程tree组件无需做任何的刷新和重新初始化噢~。只需要把设置好后台id的新增数据列表作为参数传给done进行调用即可。

看下面的示例演示:

在这里插入图片描述

提交的数据,组件内部实现了格式封装:

{
  "insert": [
    {
      "name": "新节点12",
      "parentId": "aaa"
    },
    {
      "name": "新节点11",
      "parentId": "bbb",
      "children": [
        {
          "name": "新节点223"
        }
      ]
    }
  ],
  "update": [
    {
      "id": "b11",
      "name": "b11112"
    },
    {
      "id": "b22",
      "name": "b2233"
    }
  ],
  "delete": [
    "55",
    "66",
    "77"
  ]
}

下面看具体的实现。

ts类型调整

export interface IFlatTreeNode extends ITreeNode {
  ...
  // 内部append方法,内部实现用的,不提供给用户,这里会接收一个data
  append: (child: SingleEditNode, data: IFlatTreeNode[]) => IFlatTreeNode
  // 内部删除方法
  remove: (flatData: IFlatTreeNode[], treeData: ITreeNode[]) => IFlatTreeNode[]
  ...
  temporary?: boolean // 新增未提交后端则属于临时节点
  backId?: IdType // 对应后端模型的id
}

这里调整了新增节点的类型为SingleEditNode,因为用户每次执行append操作只会新增一个节点,也就是叶子节点,只是叫法上进行了一下优化,有单节点就有嵌套节点,后续涉及。

remove方法增加了被移除扁平化节点列表的返回值,这便于收集节点编辑后的变更项。

然后增加了一个temporary的可选属性,来表明一个节点是否属于临时节点,注意,临时节点是临时新增的,只有前端id,并没有同步到后台。

最后增加了一个backId属性,它对应后台模型的id。这里我们区分前端id和后台id

同样,提供给用户操作的接口中append方法的入参类型也调整下:

export interface ITreeNodeOperation {
  append: (node: SingleEditNode) => void // 新增子节点
  ...
}

将原先的ILeafNode改名为SingleEditNode

// 新增的节点类型(这里只考虑每次新增一级,也就是叶节点)
export interface SingleEditNode {
  id?: IdType // 用户可传id,但要避免冲突
  label: string // 节点名称
}

同时创建一个NestedEditNode节点类型,用于变更数据进行同步插入时使用,用它作为入参。我们可以基于多个节点分支进行子节点新增,则会对应一个NestedEditNode数组,每个分支则是一个嵌套结构,为此,做如下定义:

export interface NestedEditNode {
  id?: IdType // 用户可传id,但要避免冲突
  label: string
  parentId?: IdType // 最外层节点会关联一个父节点
  // 嵌套结构,满足在新增的临时节点上继续新增子节点的场景
  children?: NestedEditNode[]
}

我们为从上一次保存点开始做的节点编辑操作的变更(也就是脏数据)创建一个类型:

export interface DirtyData {
  insert?: NestedEditNode[]
  update?: SingleEditNode[]
  delete?: IdType[]
}

这里我们详细划分出了要插入、更新以及删除的数据,并指定了各自的类型,让用户一目了然。

最后,我们提供一个保存变更的接口:

export interface SaveChanged {
  (dirtyData: DirtyData, done: (insertDataWithId?: NestedEditNode[]) => void): void
}

这里会接收组件内部处理好的脏数据,也允许用户执行一个done回调来flush脏数据,注意,回调时如果有新增的变更,要把后台模型生成的节点id同步给前端。

utils工具调整

调整下节点移除方法的返回值:

在这里插入图片描述

节点append方法调整:

在这里插入图片描述

新增一级节点方法调整:

在这里插入图片描述

节点后端id初始化:

在这里插入图片描述

提供一个对新增的节点列表由嵌套结构进行拍平处理得到扁平化的id列表结构的功能函数:

在这里插入图片描述

index.tsx调整

...
// 新增节点数组
const insertNodes = [] as IFlatTreeNode[]
// 更新节点数组
const updateNodes = [] as IFlatTreeNode[]
// 删除节点id
const deleteNodeIds = [] as IdType[]

...

const _handleNodeRemove = (node: IFlatTreeNode) => {
  // 删除并获取移除的节点列表
  const removedFlatNodes = node.remove(originalFlatData, data)
  // 如果删除的节点原先是新增的,也要从新增列表中移除掉
  removedFlatNodes.forEach((item) => {
    const index = insertNodes.findIndex((n) => n.id === item.id)
    if (index >= 0) {
      insertNodes.splice(index, 1)
    }
    // 如果删除的是非临时节点,则添加到删除列表中
    if (!item.temporary) {
      deleteNodeIds.push(item.backId!)
    }
  })
}

// 返回节点操作方法的函数
const nodeOperation = (node: IFlatTreeNode): ITreeNodeOperation => {
  // 注意,这里不应该直接给用户提供node,而是要包成对外公开的ITreeNodeOperation,遵循迪米特法则!!
  return {
    ...,
    append(newNode: SingleEditNode) {
      // 返回新的扁平化节点
      const newFlatNode = node.append(newNode, originalFlatData)
      _startEdit(newFlatNode)
      // 插入新节点到临时新增数组中
      insertNodes.push(newFlatNode)
    },
    async remove(call: RemoveCall) {
      // 如果是临时节点直接删除
      if (node.temporary) {
        _handleNodeRemove(node)
      } else {
        try {
          await call({
            id: node.backId, // 非临时节点一定有后端id
            ...
          } as IFlatTreeNode)
          _handleNodeRemove(node)
        } catch (msg) {
          console.warn(msg)
        }
      }
    }
  }
}
// 结束节点标签名编辑的函数
const finishEdit = (node: IFlatTreeNode) => {
  // 如果输入不为空,则绑定节点的名称
  // 同时更新关联的原始节点的name,
  if (editLabel.value !== '') {
    const name = labelName as 'label'
    // 获取编辑前的节点名称
    const oldLabel = node[name]
    // 如果编辑已存在的节点,且名称发生变更则添加到更新列表中
    if (!node.temporary && editLabel.value !== oldLabel) {
      updateNodes.push(node)
    }
    ...
  }
  ...
}
...

const saveDirty = (call: SaveChanged) => {
  // 整理dirty数据
  const name = labelName as 'label'
  // 结构化转换
  const insertData = [] as NestedEditNode[]
  if (insertNodes.length > 0) {
    const nodeMap = {} as any
    // 按照层级排序
    insertNodes.sort((n1, n2) => n1.level - n2.level)
    insertNodes.forEach((n) => {
      const node = {} as NestedEditNode
      node[name] = n[name]
      nodeMap[n.id] = node
      if (n.parent) {
        // 如果父节点是临时的,获取父节点并添加到其children中
        if (n.parent.temporary) {
          const parent = nodeMap[n.parent.id] as NestedEditNode
          parent.children = parent.children || []
          parent.children.push(node)
        } else {
          // 父节点是已存在的,则绑定父节点id并添加到插入列表中
          node.parentId = n.parent.backId
          insertData.push(node)
        }
      } else {
        insertData.push(node)
      }
    })
  }
  // 构造变更数据
  const dirtyData = {
    insert: insertData,
    update: updateNodes.map((n) => {
      const node = {} as SingleEditNode
      node.id = n.backId
      node[name] = n[name]
      return node
    }) as SingleEditNode[],
    delete: [...deleteNodeIds]
  } as DirtyData
  // 检测无变更数据的情况
  if (dirtyData.insert!.length === 0 && dirtyData.update!.length === 0 && dirtyData.delete!.length === 0) {
    console.warn('没有要保存的数据')
    return
  }
  // 回调处理
  call(dirtyData, (insertDataWithId?: NestedEditNode[]) => {
    // 这里是与后端对接后的回调处理
    const insertLength = insertNodes!.length
    let insertIds = [] as IdType[]
    if (insertDataWithId && insertDataWithId.length > 0) {
      // 扁平化处理后台同步过来的插入数据,得到后台的id列表
      insertIds = flattenNestedNodeIds(insertDataWithId)
    }
    // 检查是否长度一致
    if (insertLength !== insertIds.length) {
      throw Error('要同步的新增数据id列表长度不一致')
    }
    if (insertLength > 0) {
      // 后台id同步到前端
      insertNodes.forEach((n, i) => {
        n.backId = insertIds![i]
        delete n.temporary
        console.log('已同步新增节点id.')
      })
    }
    // 重置临时列表
    insertNodes.length = 0
    updateNodes.length = 0
    deleteNodeIds.length = 0
  })
}

const exposeProps: ExposeProps = {
  saveDirty,
  appendTop(newNode: SingleEditNode) {
    // 内部方法调用
    const newFlatNode = appendTop(newNode, originalFlatData, data, optionProps)
    _startEdit(newFlatNode)
    // 将新插入的节点添加到插入列表
    insertNodes.push(newFlatNode)
  }
}
expose(exposeProps)
...      
  • 19
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我! 基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip基于Django+python编写开发的毕业生就业管理系统支持学生教师角色+db数据库(毕业设计新项目).zip
毕设新项目基于python3.7+django+sqlite开发的学生就业管理系统源码+使用说明(含vue前端源码).zip 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我! 学生就业管理系统(前端) ## 项目开发环境 - IDE: vscode - node版本: v12.14.1 - npm版本: 6.13.4 - vue版本: @vue/cli 4.1.2 - 操作系统: UOS 20 ## 1.进入项目目录安装依赖 ``` npm install ``` ## 2.命令行执行进入UI界面进行项目管理 ``` vue ui ``` ## 3.编译发布包(请注意编译后存储路径) #### PS:需要将编译后的包复制到后端项目的根目录下并命名为'static' 学生就业管理系统(后端) ## 1.项目开发环境 - IDE: vscode - Django版本: 3.0.3 - Python版本: python3.7.3 - 数据库 : sqlite3(测试专用) - 操作系统 : UOS 20 ## 2.csdn下载本项目并生成/安装依赖 ``` pip freeze > requirements.txt pip install -r requirements.txt ``` ## 3.项目MySQL数据库链接错误 [点击查看解决方法](https://www.cnblogs.com/izbw/p/11279237.html)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Java小卷

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

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

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

打赏作者

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

抵扣说明:

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

余额充值