浏览代码

feat: 添加历史回退功能

lianghongjie 1 年之前
父节点
当前提交
0d5721387d

+ 1 - 1
package.json

@@ -61,7 +61,7 @@
     "proto.gl": "^1.0.0",
     "qrcode": "^1.5.3",
     "queen3d": "^0.0.80",
-    "queenjs": "^1.0.0-beta.74",
+    "queenjs": "^1.0.0-beta.75",
     "rimraf": "^3.0.2",
     "scp2": "^0.5.0",
     "swiper": "^8.4.4",

+ 1 - 1
src/modules/editor/components/CompUI/basicUI/Page/PageForm.tsx

@@ -25,7 +25,7 @@ export const PageForm = defineComponent({
   setup(props) {
     const { actions } = useEditor();
     function changeVal(e: { dataIndex: string; value: any }) {
-      actions.updateCompDataByForm(props.component, e.dataIndex, e.value);
+      actions.updateCompData(props.component, e.dataIndex, e.value);
     }
 
     return () => {

+ 14 - 3
src/modules/editor/components/CompUI/basicUI/Text/component.tsx

@@ -7,10 +7,11 @@ import { FontColor, FontFamily, FontSize } from "@ckeditor/ckeditor5-font";
 import { Link } from "@ckeditor/ckeditor5-link";
 import { Paragraph } from "@ckeditor/ckeditor5-paragraph";
 import { css } from "@linaria/core";
-import { defineComponent } from "vue";
+import { defineComponent, watchEffect } from "vue";
 import { string } from "vue-types";
 import { useCompData } from ".";
 import { View } from "../View";
+import { DesignComp } from "@/modules/editor/objects/DesignTemp/DesignComp";
 
 export const Component = defineComponent({
   props: {
@@ -18,7 +19,7 @@ export const Component = defineComponent({
   },
   setup(props) {
     const comp = useCompData(props.compId);
-    const { store } = useEditor();
+    const { store, helper, actions } = useEditor();
     const config = {
       plugins: [
         Essentials,
@@ -53,6 +54,12 @@ export const Component = defineComponent({
 
     let editorInstance: InlineEditor;
 
+    watchEffect(() => {
+      if (!store.textEditingState) {
+        editorInstance?.setData(comp.value);
+      }
+    });
+
     return () => (
       <View
         class={[textStyle, store.currCompId === props.compId && "drag-disable"]}
@@ -62,8 +69,12 @@ export const Component = defineComponent({
           class={textStyle}
           editor={InlineEditor}
           onBlur={() => {
+            actions.updateCompData(
+              helper.findComp(props.compId) as DesignComp,
+              "value",
+              editorInstance.getData()
+            );
             store.setTextEditingState(false);
-            comp.value = editorInstance.getData();
           }}
           onFocus={() => {
             store.setTextEditingState(true);

+ 1 - 1
src/modules/editor/components/CompUI/defines/createAttrsForm.tsx

@@ -158,7 +158,7 @@ export function createAttrsForm(valueColumns: ColumnItem[]) {
     setup(props) {
       const { store, actions } = useEditor();
       function changeVal(e: { dataIndex: string; value: any }) {
-        actions.updateCompDataByForm(store.currComp, e.dataIndex, e.value);
+        actions.updateCompData(store.currComp, e.dataIndex, e.value);
       }
 
       return () => {

+ 1 - 2
src/modules/editor/components/TipIcons/index.ts

@@ -9,7 +9,6 @@ import {
   IconLayerUp,
 } from "@/assets/icons";
 import {
-  IconBtnNext,
   IconCamera,
   IconCube,
   IconDelete,
@@ -18,7 +17,7 @@ import {
   IconLock,
   IconRedo,
   IconUndo,
-  IconUnlock,
+  IconUnlock
 } from "@queenjs/icons";
 import { createTipIcon } from "./create";
 

+ 61 - 185
src/modules/editor/controllers/HistoryCtrl/HistoryController.ts

@@ -1,229 +1,105 @@
-import { get } from "lodash";
+import { set } from "lodash";
 import { StateRoot } from "queenjs";
 
+type RecordOptions = { combine?: boolean };
 class State extends StateRoot {
-  lenth = 0; //操作栈的长度
-  total = 50; //操作栈总长度
-  cap = 100; //操作栈的容量
+  currLen = 0; //操作栈的长度
+  maxLen = 100; //操作栈总长度
 
   opIndex = -1; //操作栈的指针
-  saveIndex = -1; //保存的指针
-  enable = true;
 
-  canSave = this.computed((state) => {
-    return state.opIndex != state.saveIndex;
-  });
   canUndo = this.computed((state) => {
-    return state.enable && state.opIndex >= 0;
+    return state.opIndex >= 0;
   });
   canRedo = this.computed((state) => {
-    return state.enable && state.opIndex + 1 < state.lenth;
+    return state.opIndex < state.currLen - 1;
   });
 }
 
 export class HistoryController {
-  combine = false;
-  _group = false;
   state = new State().reactive();
-  queue: Action[] = [];
-  groupQueue: Action[] = [];
-
-  constructor(cap = 100) {
-    this.state.cap = 2 * cap;
-
-    //最大缓存行为个数
-    this.state.total = cap;
-  }
-
-  saved() {
-    this.state.saveIndex = this.state.opIndex;
-  }
+  queue: GroupAction[] = [];
+  cacheGroupAction = new GroupAction();
 
+  // 添加缓存记录
   record(action: Action) {
-    if (!action) return;
-    if (this.group) {
-      this.groupQueue.push(action);
-      return;
-    }
-    const state = this.state;
-    if (!state.enable) return;
-    const lastAction = this.queue[state.opIndex];
-
-    // 同时满足[可合并状态|操作指针指向栈顶|同对象的同种操作]
-    if (
-      lastAction?.combine &&
-      this.queue.length === state.opIndex + 1 &&
-      action.name === lastAction.name &&
-      action.root === lastAction.root
-    ) {
-      lastAction.value = action.value;
-      lastAction.combine = action.combine;
-      return;
-    }
-
-    let index = state.opIndex + 1;
-    if (index >= state.cap) {
-      //大于容量了
-      this.queue = this.queue.slice(state.total);
-      index = this.state.total;
-    }
-
-    this.state.opIndex = index;
-    this.queue[index] = action;
-    this.state.lenth = index + 1;
-
-    if (this.queue.length > index + 1) {
-      //丢掉回退的部分操作
-      this.queue.splice(index + 1, this.queue.length - index - 1);
+    this.cacheGroupAction.record(action);
+  }
+
+  // 保存缓存记录到历史栈中
+  submit(action?: Action) {
+    const { state, queue, cacheGroupAction } = this;
+    if (action) this.record(action);
+    if (!cacheGroupAction.actions.length) return;
+
+    // 将缓存操作记录保存到当前指针的下一栈中
+    queue[++state.opIndex] = cacheGroupAction;
+    // 设置栈的长度为指针的长度,舍弃后面的记录
+    queue.length = state.opIndex + 1;
+    // 若栈长度超过上限, 舍弃之前的记录
+    if (queue.length > state.maxLen) {
+      queue.splice(0, queue.length - state.maxLen);
+      state.opIndex = state.maxLen - 1;
     }
+    // 更新当前长度状态
+    state.currLen = queue.length;
+    // 更新当前缓存GroupAction
+    this.cacheGroupAction = new GroupAction();
   }
 
   undo() {
-    const state = this.state;
-    if (!state.canUndo) return;
-
-    if (state.opIndex < 0) return; //已经退到第一步了
-
-    const action = this.queue[state.opIndex];
-    action.undo();
-
-    state.opIndex = state.opIndex - 1;
+    if (!this.state.canUndo) return;
+    this.queue[this.state.opIndex--].undo();
   }
 
   redo() {
-    const state = this.state;
-    if (!state.canRedo) return;
-
-    if (state.opIndex >= this.queue.length - 1) return; //已经是最后一步操作了
-
-    const action = this.queue[state.opIndex + 1];
-    action.redo();
-
-    state.opIndex = state.opIndex + 1;
-  }
-
-  get group() {
-    return this._group;
-  }
-  set group(state) {
-    this._group = state;
-    if (!state) {
-      if (this.groupQueue.length) {
-        this.record(
-          this.groupQueue.length == 1
-            ? this.groupQueue[0]
-            : (new GroupAction(this.groupQueue) as any)
-        );
-      }
-    }
-    this.groupQueue = [];
-  }
-
-  removeHead() {
-    const state = this.state;
-    if (state.opIndex > 0) {
-      state.opIndex = state.opIndex - 1;
-      this.queue.splice(this.queue.length - 1, 1);
-    }
+    if (!this.state.canRedo) return;
+    this.queue[++this.state.opIndex].redo();
   }
 
   //清除操作
   clear() {
-    const len = this.state.opIndex - this.state.saveIndex;
-    if (len !== 0) {
-      Array.from({ length: Math.abs(len) }).forEach(() => {
-        len > 0 ? this.undo() : this.redo();
-      });
-    }
     this.queue = [];
+    this.state.currLen = 0;
     this.state.opIndex = -1;
-    this.state.lenth = 0;
-    this.state.saveIndex = -1;
+    this.cacheGroupAction = new GroupAction();
   }
 }
 
 export class Action {
-  combine = false;
-  name: string;
-  root: any;
-  value: any;
-  valueOld: any;
-
-  constructor(root: any, name: string, value?: any) {
-    const [, path] = name.split(":");
-    this.name = name;
-    this.root = root;
-    this.valueOld = get(root, path.split("."));
-    this.value = value;
-  }
-
-  async redo() {
-    this._action("redo", this.value);
-  }
-
-  async undo() {
-    this._action("undo", this.valueOld);
+  constructor(
+    public type: "set" | "delete",
+    public root: any,
+    public path: string,
+    public value?: any,
+    public oldValue?: any
+  ) {}
+  undo() {
+    set(this.root, this.path, this.oldValue);
   }
-
-  _action(actionType: "redo" | "undo", value: any) {
-    const [, type, , , parentPath = "", attrName = ""] =
-      this.name.match(/^(.+):(((.+)\.)?(.+))?$/) || [];
-    let paths: string | string[] = parentPath.split(".");
-    if (type == "add" && attrName) {
-      if (parentPath) {
-        +attrName >= 0 && paths.push(attrName);
-      } else {
-        paths = attrName;
-      }
-    }
-    const parent = get(this.root, paths) || this.root;
-    switch (type) {
-      case "add":
-        if (parent instanceof Array) {
-          if (actionType === "redo") {
-            parent.push(value);
-          } else {
-            parent.pop();
-          }
-        } else {
-          if (actionType === "redo") {
-            parent[attrName] = value;
-          } else {
-            delete parent[attrName];
-          }
-        }
-        break;
-      case "set":
-        parent[attrName] = value;
-        break;
-      case "remove":
-        if (parent instanceof Array) {
-          if (actionType === "redo") {
-            parent.splice(+attrName, 1);
-          } else {
-            parent.splice(+attrName, 0, value);
-          }
-        } else {
-          if (actionType === "redo") {
-            delete parent[attrName];
-          } else {
-            parent[attrName] = value;
-          }
-        }
-        break;
-    }
+  redo() {
+    set(this.root, this.path, this.value);
   }
 }
 
 export class GroupAction {
-  group: Action[];
-  constructor(group: Action[]) {
-    this.group = group;
+  actions: Action[] = [];
+  record(action: Action, options?: RecordOptions) {
+    const lastAction = this.actions.at(-1);
+    if (
+      options?.combine &&
+      lastAction?.root === action.root &&
+      lastAction?.path === action.path
+    ) {
+      this.actions[this.actions.length - 1] = action;
+    } else {
+      this.actions.push(action);
+    }
   }
   undo() {
-    this.group.reverse().forEach((d) => d.undo());
+    [...this.actions].reverse().forEach((act) => act.undo());
   }
   redo() {
-    this.group.forEach((d) => d.redo());
+    this.actions.forEach((d) => d.redo());
   }
 }

+ 16 - 20
src/modules/editor/controllers/HistoryCtrl/index.ts

@@ -1,4 +1,3 @@
-import { cloneDeep } from "lodash";
 import { AnyFun } from "queenjs/typing";
 import { EditorModule } from "../../module";
 import { Action, HistoryController } from "./HistoryController";
@@ -8,21 +7,24 @@ export class HistoryCtrl {
   historyActionDoing = false;
 
   constructor(protected module: EditorModule, historyTotal = 50) {
-    this.history = new HistoryController(historyTotal);
+    this.history = new HistoryController();
+    this.history.state.maxLen = historyTotal;
   }
 
-  onChange(root: any, type: string, paths: string[], value: any) {
+  record(
+    root: any,
+    type: Action["type"],
+    paths: string[],
+    value: any,
+    oldValue: any
+  ) {
     if (this.historyActionDoing) {
-      const action = new Action(
-        root,
-        `${type}:${paths.join(".")}`,
-        cloneDeep(value)
-      );
+      const action = new Action(type, root, paths.join("."), value, oldValue);
       this.history.record(action);
     }
   }
 
-  proxyActions(actNames: string[]) {
+  bindActions(actNames: string[]) {
     const actions: any = this.module.actions;
     actNames.forEach((actName) => {
       const action = actions[actName];
@@ -31,24 +33,18 @@ export class HistoryCtrl {
   }
 
   async proxyAction(action: AnyFun, ...args: any[]) {
+    const { history } = this;
     if (this.historyActionDoing) {
       return await action(...args);
     }
-    const { history } = this;
     try {
       this.historyActionDoing = true;
-      history.group = true;
       await action(...args);
-      history.group = false;
+      history.submit();
     } catch (error) {
-      if (history.groupQueue.length) {
-        history.group = false;
-        history.undo();
-        history.queue.pop();
-        history.state.lenth--;
-      } else {
-        history.group = false;
-      }
+      console.error(error);
+      history.cacheGroupAction.undo();
+      history.cacheGroupAction.actions = [];
     } finally {
       this.historyActionDoing = false;
     }

+ 3 - 2
src/modules/editor/module/actions/edit.ts

@@ -104,9 +104,10 @@ export const editActions = EditorModule.action({
   // 设置组件变换
   setCompTransform(comp: DesignComp, transform: Layout["transform"]) {
     comp.layout.transform = transform;
+    console.log(comp);
   },
 
-  updateCompDataByForm(comp: DesignComp, path: string, value: any) {
+  updateCompData(comp: DesignComp, path: string, value: any) {
     set(comp, path, value);
   },
 
@@ -122,7 +123,7 @@ export const editActions = EditorModule.action({
   // 清除组件变换
   clearCompTransform(comp: DesignComp) {
     comp.layout.margin = "";
-    comp.layout.transform = {};
+    comp.layout.transform = undefined;
   },
   // 设置组件锁定状态
   setCompLock(comp: DesignComp) {

+ 4 - 4
src/modules/editor/module/actions/init.ts

@@ -1,5 +1,5 @@
-import { createProxyEffect } from "queenjs";
 import { EditorModule } from "..";
+import { createProxyEffect } from "../../objects/ProxyStore/create";
 import { EditorMode } from "../../typings";
 import { editActions } from "./edit";
 
@@ -7,12 +7,12 @@ export const initActions = EditorModule.action({
   // 模块初始化
   init() {
     const { historyCtrl } = this.controls;
-    historyCtrl.proxyActions(Object.keys(editActions));
+    historyCtrl.bindActions(Object.keys(editActions));
     this.controls.compUICtrl.init();
 
-    createProxyEffect(this.store, (type, paths, value) => {
+    createProxyEffect(this.store, (type, paths, value, oldValue) => {
       if (paths[0] === "designData" || paths[0] === "currCompId") {
-        historyCtrl.onChange(this.store, type, paths, value);
+        historyCtrl.record(this.store, type, paths, value, oldValue);
       }
     });
   },

+ 6 - 3
src/modules/editor/module/index.ts

@@ -1,17 +1,18 @@
 import { Dict_Apis } from "@/dict";
+import { UploadController } from "@queenjs/controllers";
 import { ModuleRoot } from "queenjs";
 import components from "../components";
 import { CompUICtrl } from "../controllers/CompUICtrl";
 import { HistoryCtrl } from "../controllers/HistoryCtrl";
 import { ImagePickController } from "../controllers/ImagePickerController";
+import { TransferCtrl } from "../controllers/TransferCtrl";
+import { createProxy } from "../objects/ProxyStore/create";
 import { editActions } from "./actions/edit";
 import { ImgCompActions } from "./actions/image";
 import { initActions } from "./actions/init";
 import { helpers } from "./helpers";
 import { https } from "./https";
 import { store } from "./stores";
-import { UploadController } from "@queenjs/controllers";
-import { TransferCtrl } from "../controllers/TransferCtrl";
 
 export class EditorModule extends ModuleRoot {
   config = this.setConfig({
@@ -23,7 +24,9 @@ export class EditorModule extends ModuleRoot {
 
   actions = this.createActions([initActions, editActions, ImgCompActions]);
   https = this.createHttps(https);
-  store = this.createStore(store, { useProxy: true });
+  store = this.createStore(store, {
+    transform: (state) => createProxy(state),
+  });
   helper = this.createHelper(helpers);
 
   controls = {

+ 3 - 3
src/modules/editor/objects/DesignTemp/DesignComp.ts

@@ -32,13 +32,13 @@ export class DesignComp {
     );
   }
 
-  get isPostioned() {
+  isPostioned() {
     return this.layout.position === "absolute";
   }
-  get isTransformed() {
+  isTransformed() {
     return !isEmpty(this.layout.transform);
   }
-  get isFullWidth() {
+  isFullWidth() {
     const w = this.layout.size?.[0];
     return !w || w === 750;
   }

+ 83 - 0
src/modules/editor/objects/ProxyStore/create.ts

@@ -0,0 +1,83 @@
+import { AnyFun } from "queenjs/typing";
+import { isProxy, toRaw } from "vue";
+
+const stateWatchers = new WeakMap<object, Set<AnyFun>>();
+
+export const createProxy = <T extends object>(
+  obj: T,
+  paths: PropertyKey[] = [],
+  root?: any
+): T => {
+  if (!(obj instanceof Object) || isProxy(obj)) return obj;
+  root = root || obj;
+  for (const key in obj) {
+    const objItem = obj[key];
+    if (objItem instanceof Object) {
+      obj[key] = createProxy(objItem, [...paths, key], root);
+    }
+  }
+
+  return new Proxy(obj, {
+    get(target: any, key: PropertyKey) {
+      if (key === "__origin__") {
+        return obj;
+      }
+      return Reflect.get(target, key);
+    },
+    set(target: any, key: PropertyKey, value: any, receiver: any) {
+      const oldValue = Reflect.get(target, key, receiver);
+      const nextValue = createProxy(value, [...paths, key], root);
+      const result = Reflect.set(target, key, nextValue, receiver);
+      if (value !== oldValue) {
+        emit("set", root, [...paths, key], nextValue, oldValue);
+      }
+      return result;
+    },
+    deleteProperty(target: any, key: PropertyKey) {
+      const oldValue = Reflect.get(target, key);
+      const result = Reflect.deleteProperty(target, key);
+      if (result && oldValue !== undefined) {
+        emit("delete", root, [...paths, key], undefined, oldValue);
+      }
+      return result;
+    },
+  });
+};
+
+export function toOriginRaw(obj: any) {
+  if (isProxy(obj)) {
+    return toRaw(obj)["__origin__"];
+  } else {
+    return obj["__origin__"] || obj;
+  }
+}
+
+function emit(
+  type: "set" | "delete",
+  root: any,
+  paths: PropertyKey[],
+  value: any,
+  oldValue: any
+) {
+  const watchers = stateWatchers.get(toOriginRaw(root));
+  watchers?.forEach((watch) => watch(type, paths, value, oldValue));
+}
+
+export function createProxyEffect(
+  target: any,
+  handler: (type: "set" | "delete", paths: string[], val: any, old: any) => void
+) {
+  target = toOriginRaw(target);
+  const handlers = stateWatchers.get(target);
+  if (handlers) {
+    handlers.add(handler);
+  } else {
+    stateWatchers.set(target, new Set([handler]));
+  }
+  return {
+    stop() {
+      const handlers = stateWatchers.get(target);
+      handlers?.delete(handler);
+    },
+  };
+}

+ 1 - 1
src/modules/editor/objects/Toolbars/TreeToolbars.ts

@@ -3,7 +3,7 @@ import { ICompToolbars, toolbars } from "./default";
 export const TreeToolbars: ICompToolbars = {
   default: [
     toolbars.position.setVisible(function (comp) {
-      return comp.isPostioned;
+      return comp.isPostioned();
     }),
     toolbars.visible,
     toolbars.delete,

+ 3 - 3
src/modules/editor/objects/Toolbars/default.ts

@@ -104,7 +104,7 @@ export const toolbars = createToolbars({
   // 清除变换
   clearTransform: {
     component: TipIcons.ClearTransform,
-    getVisible: (comp) => comp.isTransformed,
+    getVisible: (comp) => comp.isTransformed(),
     onClick(comp) {
       this.actions.clearCompTransform(comp);
     },
@@ -112,7 +112,7 @@ export const toolbars = createToolbars({
   // 定位图层上移
   layerUp: {
     component: TipIcons.LayerUp,
-    getVisible: (comp) => comp.isPostioned,
+    getVisible: (comp) => comp.isPostioned(),
     onClick(comp) {
       this.actions.setCompLayer(comp, 1);
     },
@@ -120,7 +120,7 @@ export const toolbars = createToolbars({
   // 定位图层下移
   layerDown: {
     component: TipIcons.LayerDown,
-    getVisible: (comp) => comp.isPostioned,
+    getVisible: (comp) => comp.isPostioned(),
     onClick(comp) {
       this.actions.setCompLayer(comp, -1);
     },

+ 4 - 4
yarn.lock

@@ -7123,10 +7123,10 @@ queen3d@^0.0.80:
   resolved "http://124.70.149.18:4873/queen3d/-/queen3d-0.0.80.tgz#11d4c60f233fc54d810e8f912b79495e4acfb95e"
   integrity sha512-GaBzki+vcjC4JDN4olh/UI3oW6BRc1qbk1+pwUlbBN0oC+ilKNn9C64tLSEio0zWZikEtGb6A9jrUXntX1no4A==
 
-queenjs@^1.0.0-beta.74:
-  version "1.0.0-beta.74"
-  resolved "http://124.70.149.18:4873/queenjs/-/queenjs-1.0.0-beta.74.tgz#d42478d5e0ef2ad91cbd8b2ab41df4c44c1a1ae2"
-  integrity sha512-4vZkI714ZypxiBPZcnaxHqBrSDEx2lKza+atSQTAqP0Y2pmrTqwyfn6Mq9trmfhARJYi7zIi10EgSRfgZLDdBQ==
+queenjs@^1.0.0-beta.75:
+  version "1.0.0-beta.75"
+  resolved "http://124.70.149.18:4873/queenjs/-/queenjs-1.0.0-beta.75.tgz#24a3ca1cecf4c6bdefde359d1e00dfd8ab108c5f"
+  integrity sha512-sdfFHIZ4v4y3utrfmc2iegkZlXVhhrwvfovf1Z9lNhZ+08jy0H/TA8sv1KEKkWKQGZpgyk9Ytu6XvGhUACMz4g==
   dependencies:
     axios "^0.27.2"
     eventemitter3 "^4.0.7"