Explorar o código

feat: 添加操作记录

liaojiaxing hai 10 meses
pai
achega
f16a7181c6

+ 163 - 52
src/store/modules/action.ts

@@ -4,144 +4,255 @@ import { LayerEnum } from "@/enum/layerEnum";
 import { useProjectStore } from "@/store/modules/project";
 import { cloneDeep } from "lodash";
 import { CustomElement } from "#/project";
+// import { recoverRecord } from "@/utils/recover";
 
+// type RecordItem = {
+//   info: Record<string, any>;
+//   type: "add" | "update" | "delete" | "init";
+//   snapshot: ProjectInfo;
+// };
+type ActionState = {
+  // 操作记录--最大记录10条
+  records: string[];
+  // 当前操作索引
+  activeIndex: number;
+  appKey: number;
+};
 export const useAcionStore = defineStore({
-  id: 'action',
-  state() {
+  id: "action",
+  state(): ActionState {
     return {
-      // 操作记录
       records: [],
-    }
+      activeIndex: -1,
+      appKey: 0,
+    };
   },
   getters: {
     projectStore: () => useProjectStore(),
+    undoDisabled: (state) => state.activeIndex <= 0,
+    redoDisabled: (state) => state.activeIndex === state.records.length - 1,
   },
   actions: {
-    addRecord(record) {
+    initRecord() {
+      this.records = [JSON.stringify(this.projectStore.projectInfo)];
+      this.activeIndex = 0;
+    },
+    // addRecord({type, info }: RecordItem & { snapshot?: ProjectInfo}) {
+    addRecord() {
+      // 新增如果当前索引不是最后一条, 覆盖后面的记录
+      if (this.activeIndex < this.records.length - 1) {
+        this.records.splice(this.activeIndex + 1, this.records.length);
+      }
+
+      this.records.push(JSON.stringify(this.projectStore.projectInfo));
+
+      // 新增如果超过10条记录,删除最早的一条
+      if (this.records.length > 10) {
+        this.records.shift();
+        this.activeIndex--;
+      }
 
+      this.activeIndex = this.records.length - 1;
+      console.log(this.activeIndex, this.records);
     },
+    // 撤销
     actionUndo() {
-
+      if (this.activeIndex <= 0) return;
+      --this.activeIndex;
+      const projectInfo = JSON.parse(this.records[this.activeIndex]);
+      this.projectStore.updateProjectInfo(projectInfo);
+      this.appKey++;
     },
+    // 重做
     actionRedo() {
-
-    },
-    actionClear() {
-
+      if (this.activeIndex >= this.records.length - 1) return;
+      ++this.activeIndex;
+      const projectInfo = JSON.parse(this.records[this.activeIndex]);
+      this.projectStore.updateProjectInfo(projectInfo);
+      this.appKey++;
     },
+    actionClear() {},
     // 对齐
     actionAlign(type: AlignEnum) {
       const activeElements = this.projectStore.currentSelectedElements;
       switch (type) {
         case AlignEnum.Bottom: {
-          const maxY = Math.max(...activeElements.map((item) => item.container.props.y + item.container.props.height));
+          const maxY = Math.max(
+            ...activeElements.map(
+              (item) => item.container.props.y + item.container.props.height
+            )
+          );
           activeElements.forEach((item) => {
-            this.projectStore.updateElement(item.key, 'container.props.y', maxY - item.container.props.height);
+            this.projectStore.updateElement(
+              item.key,
+              "container.props.y",
+              maxY - item.container.props.height
+            );
           });
           break;
         }
         case AlignEnum.HorizontalCenter: {
-          const maxX = Math.max(...activeElements.map((item) => item.container.props.x + item.container.props.width));
-          const minX = Math.min(...activeElements.map((item) => item.container.props.x));
+          const maxX = Math.max(
+            ...activeElements.map(
+              (item) => item.container.props.x + item.container.props.width
+            )
+          );
+          const minX = Math.min(
+            ...activeElements.map((item) => item.container.props.x)
+          );
           const centerX = minX + (maxX - minX) / 2;
           activeElements.forEach((item) => {
-            this.projectStore.updateElement(item.key, 'container.props.x', centerX - item.container.props.width / 2);
+            this.projectStore.updateElement(
+              item.key,
+              "container.props.x",
+              centerX - item.container.props.width / 2
+            );
           });
           break;
         }
         case AlignEnum.VerticalCenter: {
-          const maxY = Math.max(...activeElements.map((item) => item.container.props.y + item.container.props.height));
-          const minY = Math.min(...activeElements.map((item) => item.container.props.y));
+          const maxY = Math.max(
+            ...activeElements.map(
+              (item) => item.container.props.y + item.container.props.height
+            )
+          );
+          const minY = Math.min(
+            ...activeElements.map((item) => item.container.props.y)
+          );
           const centerY = minY + (maxY - minY) / 2;
           activeElements.forEach((item) => {
-            this.projectStore.updateElement(item.key, 'container.props.y', centerY - item.container.props.height / 2);
+            this.projectStore.updateElement(
+              item.key,
+              "container.props.y",
+              centerY - item.container.props.height / 2
+            );
           });
           break;
         }
         case AlignEnum.Left: {
-          const minX = Math.min(...activeElements.map((item) => item.container.props.x));
+          const minX = Math.min(
+            ...activeElements.map((item) => item.container.props.x)
+          );
           activeElements.forEach((item) => {
-            this.projectStore.updateElement(item.key, 'container.props.x', minX);
+            this.projectStore.updateElement(
+              item.key,
+              "container.props.x",
+              minX
+            );
           });
           break;
         }
         case AlignEnum.Right: {
-          const maxX = Math.max(...activeElements.map((item) => item.container.props.x + item.container.props.width));
+          const maxX = Math.max(
+            ...activeElements.map(
+              (item) => item.container.props.x + item.container.props.width
+            )
+          );
           activeElements.forEach((item) => {
-            this.projectStore.updateElement(item.key, 'container.props.x', maxX - item.container.props.width);
+            this.projectStore.updateElement(
+              item.key,
+              "container.props.x",
+              maxX - item.container.props.width
+            );
           });
-          break
+          break;
         }
         case AlignEnum.Top: {
-          const minY = Math.min(...activeElements.map((item) => item.container.props.y));
-          console.log(this.projectStore.currentSelectedElements, minY)
+          const minY = Math.min(
+            ...activeElements.map((item) => item.container.props.y)
+          );
+          console.log(this.projectStore.currentSelectedElements, minY);
           activeElements.forEach((item) => {
-            this.projectStore.updateElement(item.key, 'container.props.y', minY);
+            this.projectStore.updateElement(
+              item.key,
+              "container.props.y",
+              minY
+            );
           });
           break;
         }
-        default: 
+        default:
       }
+      this.addRecord();
     },
     // 图层调整
     actionLayer(type: LayerEnum) {
       const activeElements = this.projectStore.currentSelectedElements;
-      const elements = cloneDeep(this.projectStore.elements.sort((a, b) => a.zIndex - b.zIndex)) as CustomElement[];
+      const elements = cloneDeep(
+        this.projectStore.elements.sort((a, b) => a.zIndex - b.zIndex)
+      ) as CustomElement[];
 
       switch (type) {
         case LayerEnum.UP: {
           activeElements.forEach((item) => {
-            const index = elements.findIndex((element) => element.key === item.key);
+            const index = elements.findIndex(
+              (element) => element.key === item.key
+            );
             if (item.zIndex === elements.length) return;
             elements.splice(index, 1);
-            elements.splice(index + 1, 0, {...item});
+            elements.splice(index + 1, 0, { ...item });
+          });
+          elements.forEach((item, index) => {
+            item.zIndex = index + 1;
           });
-          elements.forEach((item, index) => { item.zIndex = index + 1});
-          elements.forEach(item => {
-            this.projectStore.updateElement(item.key, 'zIndex', item.zIndex);
+          elements.forEach((item) => {
+            this.projectStore.updateElement(item.key, "zIndex", item.zIndex);
           });
           break;
         }
         case LayerEnum.DOWN: {
           activeElements.forEach((item) => {
-            const index = elements.findIndex((element) => element.key === item.key);
+            const index = elements.findIndex(
+              (element) => element.key === item.key
+            );
             if (item.zIndex === 1) return;
             elements.splice(index, 1);
-            elements.splice(index - 1, 0, {...item});
+            elements.splice(index - 1, 0, { ...item });
           });
-          elements.forEach((item, index) => { item.zIndex = index + 1});
-          elements.forEach(item => {
-            this.projectStore.updateElement(item.key, 'zIndex', item.zIndex);
+          elements.forEach((item, index) => {
+            item.zIndex = index + 1;
+          });
+          elements.forEach((item) => {
+            this.projectStore.updateElement(item.key, "zIndex", item.zIndex);
           });
           break;
         }
         case LayerEnum.TOP: {
           activeElements.forEach((item) => {
-            const index = elements.findIndex((element) => element.key === item.key);
+            const index = elements.findIndex(
+              (element) => element.key === item.key
+            );
             if (item.zIndex === elements.length) return;
             elements.splice(index, 1);
-            elements.push({...item});
+            elements.push({ ...item });
+          });
+          elements.forEach((item, index) => {
+            item.zIndex = index + 1;
           });
-          elements.forEach((item, index) => { item.zIndex = index + 1});
-          elements.forEach(item => {
-            this.projectStore.updateElement(item.key, 'zIndex', item.zIndex);
+          elements.forEach((item) => {
+            this.projectStore.updateElement(item.key, "zIndex", item.zIndex);
           });
           break;
         }
         case LayerEnum.BOTTOM: {
           activeElements.forEach((item) => {
-            const index = elements.findIndex((element) => element.key === item.key);
+            const index = elements.findIndex(
+              (element) => element.key === item.key
+            );
             if (item.zIndex === 1) return;
             elements.splice(index, 1);
-            elements.unshift({...item});
+            elements.unshift({ ...item });
           });
-          elements.forEach((item, index) => { item.zIndex = index + 1});
-          elements.forEach(item => {
-            this.projectStore.updateElement(item.key, 'zIndex', item.zIndex);
+          elements.forEach((item, index) => {
+            item.zIndex = index + 1;
+          });
+          elements.forEach((item) => {
+            this.projectStore.updateElement(item.key, "zIndex", item.zIndex);
           });
           break;
         }
-      }  
-    }
-  }
-})
+      }
+      this.addRecord();
+    },
+  },
+});

+ 4 - 0
src/store/modules/project.ts

@@ -23,6 +23,7 @@ type ProjectState = {
   selectedElementKeys: number[];
 };
 const defaultPage: Page = {
+  key: "1",
   name: "页面1",
   background: {
     type: "color",
@@ -98,6 +99,9 @@ export const useProjectStore = defineStore({
     updateProjectInfo(info: any) {
       Object.assign(this.projectInfo, info);
     },
+    updateProjectInfoByPath(path: string, payload: any) {
+      update(this.projectInfo, path, () => payload);
+    },
     addReferLine(line: ReferLine) {
       this.projectInfo.pages[this.activePageIndex].referLines.push(line);
     },

+ 65 - 0
src/utils/recover.ts

@@ -0,0 +1,65 @@
+import { ProjectInfo } from "#/project";
+import  { update, isArray, isPlainObject, get, isEqual } from 'lodash';
+/**
+ * 批量恢复记录
+ * 
+ * @param projectInfo 项目信息
+ * @param path 路径
+ * @param changeValue 修改值
+ * @returns 
+ */
+export const recoverRecord = (projectInfo: ProjectInfo, path: string = '', changeValue: any) => {
+  const target = get(projectInfo, path);
+  // 数组处理
+  if (isArray(changeValue)) {
+    // 判断两个数组是否一致
+    const originKeys = target.map((item: any) => item.key);
+    const changeKeys = changeValue.map((item: any) => item.key);
+    if(!isEqual(originKeys, changeKeys)) {
+      // 两个数组不一致时,移除原数组多余的数据
+      target.forEach((item: any) => {
+        if(!changeKeys.includes(item.key)) {
+          const index = target.findIndex((val: any) => val.key === item.key);
+          target.splice(index, 1);
+        }
+      });
+      // 添加新的数据
+      changeValue.forEach((item: any) => {
+        if(!originKeys.includes(item.key)) {
+          target.push(item);
+        }
+      });
+    }
+
+    // 遍历更新
+    changeValue.forEach((item: any) => {
+      const index = target.findIndex((val: any) => val.key === item.key);
+      recoverRecord(projectInfo, `${path}[${index}]`, item);
+    });
+
+    return;
+  }
+
+  // 对象处理
+  if (isPlainObject(changeValue)) {
+    // 删除多余的属性
+    if(path && Object.keys(changeValue).length < Object.keys(target || {}).length) {
+      Object.keys(target).forEach((key) => {
+        if(!Object.keys(changeValue).includes(key)) {
+          delete target[key];
+          update(projectInfo, path, () => target);
+        }
+      });
+    }
+    Object.keys(changeValue).forEach((key) => {
+      recoverRecord(projectInfo, `${path}${path && '.'}${key}`, changeValue[key]);
+    });
+    return;
+  }
+
+  // 其他
+  const originValue = get(projectInfo, path);
+  if(originValue !== changeValue) {
+    update(projectInfo, path, () => changeValue);
+  }
+}

+ 8 - 1
src/views/designer/component/ComponentWrapper.vue

@@ -38,6 +38,7 @@ import { useStageStore } from "@/store/modules/stage";
 import { useProjectStore } from "@/store/modules/project";
 import { useDraggable } from "@vueuse/core";
 import { UseDraggable } from "@vueuse/components";
+import { useAcionStore } from "@/store/modules/action";
 import { asyncComponentAll } from 'shalu-dashboard-ui';
 import Container from "@/components/Container/index.vue";
 
@@ -50,6 +51,7 @@ const component = defineAsyncComponent(
 const componentWrapperRef = ref<HTMLElement | null>(null);
 const stageStore = useStageStore();
 const projectStore = useProjectStore();
+const actionStore = useAcionStore();
 const editWapperStyle = computed(() => {
   const { width = 400, height = 260 } = componentData.container.props || {};
 
@@ -111,6 +113,7 @@ const getTip = computed(() => {
 
 let isPointDragFlag = false;
 const showNameTip = ref(true);
+let moveLeft: number;
 // 拖拽移动组件
 useDraggable(componentWrapperRef, {
   onMove: (position) => {
@@ -122,6 +125,7 @@ useDraggable(componentWrapperRef, {
     const yMoveLentgh = position.y - originPosition.top;
     const { x, y } = componentData.container.props || {};
 
+    moveLeft = Math.max(Math.abs(xMoveLength), Math.abs(yMoveLentgh));
     projectStore.updateElement(
       componentData.key,
       "container.props.x",
@@ -136,12 +140,14 @@ useDraggable(componentWrapperRef, {
   onStart: () => {
     projectStore.setSelectedElementKeys([componentData.key]);
     showNameTip.value = false;
+    moveLeft = 0;
   },
   onEnd: () => {
     showNameTip.value = true;
+    moveLeft && actionStore.addRecord(); // 记录操作
   },
 });
-const handleSelectComponent = (e: Event) => {
+const handleSelectComponent = () => {
   projectStore.setSelectedElementKeys([componentData.key]);
 };
 
@@ -230,6 +236,7 @@ const handleDragStart = (_: any, e: PointerEvent) => {
 const handleDragEnd = () => {
   isPointDragFlag = false;
   showNameTip.value = true;
+  actionStore.addRecord(); // 记录操作
 };
 </script>
 

+ 6 - 0
src/views/designer/component/LayerItem.vue

@@ -70,12 +70,14 @@ import {
   DeleteOutlined,
 } from "@ant-design/icons-vue";
 import { useProjectStore } from "@/store/modules/project";
+import { useAcionStore } from "@/store/modules/action";
 
 const props = defineProps<{
   data: CustomElement;
 }>();
 
 const projectStore = useProjectStore();
+const actionStore = useAcionStore();
 
 const layerName = ref<string>(props.data.name);
 
@@ -87,6 +89,7 @@ const handleMenuClick = ({ key }: { key: "rename" | "del" }) => {
     isEditing.value = true;
   } else {
     projectStore.removeElement(props.data.key);
+    actionStore.addRecord() // 添加记录
   }
 };
 
@@ -97,6 +100,7 @@ const handleChangeName = () => {
     return;
   };
   projectStore.updateElement(props.data.key, "name", layerName.value);
+  actionStore.addRecord() // 添加记录
 };
 
 const handleActive = () => {
@@ -105,10 +109,12 @@ const handleActive = () => {
 
 const handleLock = (locked: boolean) => {
   projectStore.updateElement(props.data.key, "locked", locked);
+  actionStore.addRecord() // 添加记录
 };
 
 const handleVisible = (visible: boolean) => {
   projectStore.updateElement(props.data.key, "visible", visible);
+  actionStore.addRecord() // 添加记录
 };
 </script>
 

+ 3 - 0
src/views/designer/component/LayerManagement.vue

@@ -48,11 +48,13 @@ import VueDraggable from "vuedraggable";
 import LayerItem from "./LayerItem.vue";
 import { useProjectStore } from "@/store/modules/project";
 import { useStageStore } from "@/store/modules/stage";
+import { useAcionStore } from "@/store/modules/action";
 
 const stageStore = useStageStore();
 const projectStore = useProjectStore();
 const filter = ref<string>("");
 const layerList = ref<CustomElement[]>([]);
+const actionStore = useAcionStore();
 
 watch(
   () => [
@@ -81,6 +83,7 @@ const dragEnd = (event: CustomEvent & {newIndex: number}) => {
     projectStore.updateElement(item.key, "zIndex", length - index);
   });
   projectStore.setSelectedElementKeys([layerList.value[event.newIndex].key]);
+  actionStore.addRecord() // 添加记录
 };
 </script>
 

+ 8 - 4
src/views/designer/component/MenuBar.vue

@@ -5,7 +5,7 @@
         <div>撤销</div>
         <div>ctrl+z</div>
       </template>
-      <Button type="text" size="small">
+      <Button type="text" size="small" :disabled="actionStore.undoDisabled" @click="actionStore.actionUndo">
         <UndoOutlined />
       </Button>
     </Tooltip>
@@ -15,7 +15,7 @@
         <div>还原</div>
         <div>ctrl+shift+z</div>
       </template>
-      <Button type="text" size="small">
+      <Button type="text" size="small" :disabled="actionStore.redoDisabled" @click="actionStore.actionRedo">
         <RedoOutlined />
       </Button>
     </Tooltip>
@@ -47,7 +47,7 @@
         <div>删除</div>
         <div>del</div>
       </template>
-      <Button type="text" size="small">
+      <Button type="text" size="small" :disabled="!projectStore.selectedElementKeys.length" @click="handleDeleteElements">
         <DeleteOutlined />
       </Button>
     </Tooltip>
@@ -132,7 +132,6 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from "vue";
 import {
   Button,
   Divider,
@@ -174,6 +173,11 @@ const handleAlignClick = ({ key }: any) => {
 const handleLayerClick = ({ key }: any) => {
   actionStore.actionLayer(key);
 };
+const handleDeleteElements = () => {
+  projectStore.selectedElementKeys.forEach((key) => {
+    projectStore.removeElement(key);
+  });
+}
 </script>
 
 <style scoped></style>

+ 3 - 0
src/views/designer/component/PageConfig.vue

@@ -7,8 +7,10 @@ import { computed } from 'vue';
 import { CusForm } from 'shalu-dashboard-ui';
 import { useProjectStore } from '@/store/modules/project';
 import { isEqual, omit } from 'lodash';
+import { useAcionStore } from '@/store/modules/action';
 
 const projectStore = useProjectStore();
+const actionStore = useAcionStore();
 const formItems = computed(() => [
   {
     label: '项目名称',
@@ -49,6 +51,7 @@ const handleChange = (value: Record<string, any>) => {
     projectStore.setCurrentPageBackground(value.background);
   };
   projectStore.updateProjectInfo(omit(value, ['background']));
+  actionStore.addRecord();
 };
 
 </script>

+ 6 - 14
src/views/designer/component/Stage.vue

@@ -12,7 +12,6 @@
         ondragover="return false"
         :style="getStyles.canvasStyle"
         @drop="handleDrop"
-        @click="handleCanvasClick"
       >
         <ComponentWrapper 
           v-for="item in projectStore.elements"
@@ -33,12 +32,14 @@ import { useStageStore } from "@/store/modules/stage";
 import { useProjectStore } from "@/store/modules/project";
 import { useScroll } from "@vueuse/core";
 import ComponentWrapper from "./ComponentWrapper.vue";
+import { useAcionStore } from "@/store/modules/action";
 
 const stageWrapperRef: Ref<HTMLElement | null> = ref(null);
 const stageRef: Ref<HTMLElement | null> = ref(null);
 const canvasRef: Ref<HTMLElement | null> = ref(null);
 const stageStore = useStageStore();
 const projectStore = useProjectStore();
+const actionStore = useAcionStore();
 
 const STAGE_SCALE = 3;
 const getStyles = computed(() => {
@@ -128,7 +129,8 @@ const initScale = () => {
     maxScale = (windowWidth / width).toFixed(2) as unknown as number;
   }
 
-  stageStore.setScale(scale > maxScale ? maxScale : scale);
+  const result = scale > maxScale ? maxScale : scale;
+  stageStore.setScale(result > 0.1 ? result : 0.1);
 };
 
 /* 设置舞台位置-默认居中 */
@@ -161,21 +163,11 @@ const handleDrop = (e: DragEvent) => {
         }
       }
     };
-    projectStore.addElement(compData); 
+    projectStore.addElement(compData);
+    actionStore.addRecord(); // 记录操作
   }
 };
 
-// 点击画布,移除所有组件选中状态
-const handleCanvasClick = (e: MouseEvent) => {
-  // if(
-  //   !e?.target?.closest(".edit-box") 
-  //   && !e?.target?.closest(".component-content")
-  //   && !e?.target?.closest(".component-wrapper")
-  // ) {
-  //   projectStore.clearAllSelectedElement();
-  // }
-};
-
 /* 适应大小设置 */
 watch(
   () => [

+ 6 - 1
src/views/designer/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <Layout style="height: 100vh">
+  <Layout style="height: 100vh" :key="actionStore.appKey">
     <LayoutHeader style="background: #fff">
       <div class="header-left">
         <h1>{{ projectStore.projectInfo.name || "大屏标题" }}</h1>
@@ -66,6 +66,7 @@ import {
 import { DesktopOutlined, SaveOutlined, ImportOutlined, ExportOutlined } from "@ant-design/icons-vue";
 import { useProjectStore } from "@/store/modules/project";
 import { useStageStore } from "@/store/modules/stage";
+import { useAcionStore } from "@/store/modules/action";
 import { useRoute } from "vue-router";
 import { useRequest } from "vue-hooks-plus";
 import { useAppStore } from "@/store/modules/app";
@@ -83,6 +84,7 @@ const stageStore = useStageStore();
 const projectStore = useProjectStore();
 const appStore = useAppStore();
 const loading = ref(false);
+const actionStore = useAcionStore();
 
 const { run, loading: loadingPage } = useRequest(getPageDesignApi, {
   manual: true,
@@ -95,6 +97,7 @@ const { run, loading: loadingPage } = useRequest(getPageDesignApi, {
     if(appPageId) {
       projectStore.updateProjectInfo({ pageId: appPageId });
     }
+    actionStore.initRecord();
   },
   onError: (e) => {
     console.error(e);
@@ -109,6 +112,8 @@ if(route.query?.pageId && route.query?.token) {
   const { token, pageId } = route.query;
   localStorage.setItem("token", token as string);
   run({id: pageId as string});
+} else {
+  actionStore.initRecord();
 }
 
 watch(

+ 2 - 0
types/project.d.ts

@@ -52,6 +52,8 @@ declare export interface ReferLine {
 }
 
 declare interface Page {
+  // 页面id
+  key: string;
   // 页面名称
   name: string;
   // 页面背景