前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >低代码平台的撤销与重做该如何设计?

低代码平台的撤销与重做该如何设计?

作者头像
前端森林
发布2022-04-28 08:26:38
8120
发布2022-04-28 08:26:38
举报
文章被收录于专栏:前端森林前端森林

在上一篇文章文章低代码平台的属性面板该如何设计?中聊到了低代码平台的属性面板的设计,今天来聊一下画布区域的撤销、重做的设计。

撤销、重做其实是我们平时一直在用的操作。对应快捷键一般就是⌘ Z / Ctrl+Z⌘⇧ Z / Ctrl+Shift+Z。这个功能是很常见的,他可以极大的提升用户体验,提高编辑效率,但是用代码应该如何实现呢?再具体点,在我们的低代码平台,针对画布区域元素的一系列操作,又该如何去设计呢?

我们先对其中的一系列状态变更做一下分析。

默认情况下,用户在画布的一系列操作会改变整个画布的呈现状态:

在进行到某个操作时,用户是可以回退到之前的状态的,也就是撤销

当然在进行撤销操作后,用户是可以恢复这个操作的,对应的就是重做

来看下之前画布的数据结构:

代码语言:javascript
复制
const editorModule = {
  state: {
    components: [],
  },
  mutations: {
     addComponent(state, component) {
      component.id = uuidv4();
      state.components.push(component);
     },
      updateComponent(state, { id, key, value, isProps }) {
        const updatedComponent = state.components.find(
          (component) => component.id === (id || state.currentElement)
        );
        if (updatedComponent) {
          if (isProps) {
            updatedComponent.props[key] = value;
          } else {
            updatedComponent[key] = value;
          }

        }
    },
    deleteComponent(state, id) {
      state.components = state.components.filter(
        (component) => component.id !== id
      );
    },
  },
}

对应操作:

  • 添加组件:addComponent
  • 更新组件:updateComponent
  • 删除组件:deleteComponent

结合上面的三张图,不难想到我们要单独维护一份数据来存储变更记录,执行撤销重做操作时就是在这份变更记录取出已有的数据,然后去更新原来的components。在原有的state中添加:

代码语言:javascript
复制
// 变更记录
histories: [],
// 游标,用来标记变更的位置
historyIndex: -1,

在画布区域操作(添加、删除、更新)时,更新原有组件数据的同时,也要维护变更记录。

我们需要封装一个更新变更记录的方法updateHistory

正常情况下其实只用往histories添加记录就可以了:

代码语言:javascript
复制
const updateHistory = (state, historyRecord) => {
  state.histories.push(historyRecord);
}

在之前的添加组件更新组件删除组件节点做一下调整:

添加组件

添加组件的同时往histories添加一项changeTypeadd的组件数据,不过这里的component要做下深拷贝:

代码语言:javascript
复制
 addComponent(state, component) {
    component.id = uuidv4();
    state.components.push(component);
    updateHistory(state, {
      id: uuidv4(),
      componentId: component.id,
      changeType: "add",
      data: cloneDeep(component),
    });
 }

更新组件

更新组件时向histories添加一项changeTypemodify的组件数据,同时要把新/老valuekey也添加进去:

代码语言:javascript
复制
updateComponent(state, { id, key, value, isProps }) {
    const updatedComponent = state.components.find(
      (component) => component.id === (id || state.currentElement)
    );
    if (updatedComponent) {
      if (isProps) {
        const oldValue = updatedComponent.props[key]
        updatedComponent.props[key] = value;
        updateHistory(state, {
          id: uuidv4(),
          componentId: id || state.currentElement,
          changeType: "modify",
          data: { oldValue, newValue: value, key },
        });
      } else {
        updatedComponent[key] = value;
      }

    }
},

删除组件

删除组件时往histories添加一条changeTypedelete的数据,同时要把index也做下记录,因为后面做撤销操作时是根据index重新插入到原来的位置:

代码语言:javascript
复制
deleteComponent(state, id) {
  const componentData = state.components.find(
    (component) => component.id === id
  ) as ComponentData;
  const componentIndex = state.components.findIndex(
    (component) => component.id === id
  );
  state.components = state.components.filter(
    (component) => component.id !== id
  );
  updateHistory(state, {
    id: uuidv4(),
    componentId: componentData.id,
    changeType: "delete",
    data: componentData,
    index: componentIndex,
  });
},

可以看到在添加历史记录的过程中,多了一个changeType字段来区分是什么类型的变更:

代码语言:javascript
复制
type changeType = 'add' | 'modify' | 'delete'

这个也是为后面的撤销/重做做铺垫,有了历史记录,针对不同的changeType分别执行对应的数据处理。

首先来看下撤销,也就是undo。第一步是要找到当前的游标,也就是撤销操作的位置。如果在此之前,从未有过撤销操作,也就是 historyIndex 为-1 时,这时将 historyIndex 置为历史记录的最后一项。否则就将 historyIndex--:

代码语言:javascript
复制
if (state.historyIndex === -1) {
  state.historyIndex = state.histories.length - 1;
} else {
  state.historyIndex--;
}

找到撤销的位置,下一步就是根据上一步记录到histories中的不同changeType做对应的数据处理:

代码语言:javascript
复制
const history = state.histories[state.historyIndex];

switch (history.changeType) {
  case "add":
    state.components = state.components.filter(
      (component) => component.id !== history.componentId
    );
    break;
  case "delete":
    state.components = insert(
      state.components,
      history.index,
      history.data
    );
    break;
  case "modify": {
    const { componentId, data } = history;
    const { key, oldValue } = data
    const updatedComponent = state.component.find(component => component.id === componentId)
    if(updatedComponent) {
      updatedComponent.props[key] = oldValue
    }
    break;
  }
  default:
    break;
}

如果之前是做了添加组件操作,那么撤销时对应的就是删除处理。

如果之前是做了删除处理,那么撤销时对应的就是把之前删除的组件恢复添加到原来的位置。

如果之前是对组件属性做了改动,那么撤销时对应的就是把组件对应的属性恢复到原来的值。

那么对于重做,就是撤销的逆向操作了,可以理解为就是正常的操作:

代码语言:javascript
复制
const history = state.histories[state.historyIndex];

switch (history.changeType) {
  case "add":
    state.components.push(history.data);
    break;
  case "delete":
    state.components = state.components.filter(
            (component) => component.id !== history.componentId
          );
    break;
  case "modify": {
    const { componentId, data } = history;
    const { key, newValue } = data
    const updatedComponent = state.component.find(component => component.id === componentId)
    if(updatedComponent) {
      updatedComponent.props[key] = newValue
    }
    break;
  }
  default:
    break;
}

其实到这里,一个基础的撤销重做就已经实现了。

但这是不符合使用习惯的,我们在用编辑器的时候,不可能让你无限的撤销,这个我们通过设置maxHistoryNumber来控制,调整一下之前的updateHistory

代码语言:javascript
复制
const updateHistory = (state, historyRecord) => {
  if (state.histories.length < maxHistoryNumber) {
    state.histories.push(historyRecord);
  } else {
    state.histories.shift();
    state.histories.push(historyRecord);
  }
}

当历史记录条目小于设定的最大历史条目前,正常往histories添加记录。

如果大于或等于maxHistoryNumber时,就把历史记录中最前面的一个剔除,同时把最新的这条加到历史记录的最后。

还有一个场景是:在撤销/重做的过程中,又正常对画布区域执行了操作。

这种情况,常用的做法就是把大于historyIndex的历史记录直接全部删除,同时把historyIndex置为-1,也就是初始状态。因为现在已经进入了一个新的状态分支:

代码语言:javascript
复制
if (state.historyIndex !== -1) {
  state.histories = state.histories.slice(0, state.historyIndex);
  state.historyIndex = -1;
}

至此,低代码平台的撤销/重做的设计思路就分享结束了。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端森林 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
腾讯云微搭低代码
微搭低代码是一个高性能的低代码开发平台,用户可通过拖拽式开发,可视化配置构建 PC Web、H5 和小程序应用。 支持打通企业内部数据,轻松实现企业微信管理、工作流、消息推送、用户权限等能力,实现企业内部系统管理。 连接微信生态,和微信支付、腾讯会议,腾讯文档等腾讯 SaaS 产品深度打通,支持原生小程序,助力企业内外部运营协同和营销管理。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档