Bladeren bron

feat: 完成图层编辑功能

liaojiaxing 10 maanden geleden
bovenliggende
commit
17d4eca83f

+ 5 - 13
src/components/Charts/Line/BasicLine/src/props.ts

@@ -52,7 +52,7 @@ export const defaultPropsValue: EChartsOption = {
   width: 400,
   height: 260,
   title: {
-    text: '标题',
+    text: '折线图标题',
     left: 'center',
     top: 8,
     textStyle: {
@@ -97,18 +97,10 @@ export const defaultPropsValue: EChartsOption = {
   dataset: {
     dimensions: ['month', 'city', 'city2'],
     source: [
-      { month: '一月', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
-      { month: '二月', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
-      { month: '三月', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
-      { month: '四月', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
-      { month: '五月', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
-      { month: '六月', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
-      { month: '七月', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
-      { month: '八月', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
-      { month: '九月', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
-      { month: '十月', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
-      { month: '十一月', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
-      { month: '十二月', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
+      { month: '纵轴A', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
+      { month: '纵轴B', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
+      { month: '纵轴C', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
+      { month: '纵轴D', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
     ],
   }
 }

+ 15 - 6
src/store/modules/project.ts

@@ -3,6 +3,7 @@ import type { ProjectInfo, Page, ReferLine, CustomElement } from "#/project";
 import type { ComponentType } from "@/components";
 import componentAll from '@/components';
 import { ScreenFillEnum } from "@/enum/screenFillEnum";
+import { update } from "lodash";
 
 type ProjectState = {
   projectInfo: ProjectInfo;
@@ -105,6 +106,8 @@ export const useProjectStore = defineStore({
         name: element.name + index,
         zIndex: elements.length + 1,
         props: defaultPropsValue,
+        visible: true,
+        locked: false,
         position: {
           x: element.position.x - (width / 2),
           y: element.position.y - (height / 2)
@@ -114,10 +117,20 @@ export const useProjectStore = defineStore({
       this.selectedElementKeys = [element.key];
     },
     // 更新组件
-    updateElement(key: number, payload: Record<string, any>) {
+    updateElement(key: number, path: string, payload: any) {
       const element = this.projectInfo.pages[this.activePageIndex].elements.find((item) => item.key === key);
+      // 如果是锁定状态不能修改宽高 位置
+      if(element && element.locked && ['props.width', 'props.height', 'position'].includes(path)) return;
+
       if (element) {
-        Object.assign(element, payload);
+        update(element, path, () => payload);
+      }
+    },
+    // 删除组件
+    removeElement(key: number) {
+      const index = this.projectInfo.pages[this.activePageIndex].elements.findIndex((item) => item.key === key);
+      if (index !== -1) {
+        this.projectInfo.pages[this.activePageIndex].elements.splice(index, 1);
       }
     },
     // 设置临时添加组件数据
@@ -135,10 +148,6 @@ export const useProjectStore = defineStore({
     setSelectedElementKeys(keys: number[]) {
       this.selectedElementKeys = keys;
     },
-    // 删除选中的元素
-    deleteSelectedElement(keys: number[]) {
-      this.selectedElementKeys = this.selectedElementKeys.filter((key) => !keys.includes(key));
-    },
     // 删除所有选中的元素
     clearAllSelectedElement() {
       this.selectedElementKeys = [];

+ 21 - 18
src/views/designer/component/ComponentWrapper.vue

@@ -8,6 +8,7 @@
       <component :is="component" v-bind="componentData.props" />
     </div>
     <div v-if="showEditBox" class="edit-box" :style="editWapperStyle">
+      <span class="name-tip">{{ componentData.name }}</span>
       <UseDraggable
         v-for="item in dragPointList"
         :key="item"
@@ -15,7 +16,7 @@
         @start="handleDragStart"
         @end="handleDragEnd"
       >
-        <span class="edit-box-point" :class="item"></span>
+        <span v-if="!componentData.locked" class="edit-box-point" :class="item"></span>
       </UseDraggable>
     </div>
   </div>
@@ -73,23 +74,21 @@ let isPointDragFlag = false;
 // 拖拽移动组件
 useDraggable(componentWrapperRef, {
   onMove: (position) => {
-    if(isPointDragFlag) return;
+    if (isPointDragFlag) return;
 
     const originPosition = componentWrapperRef.value!.getBoundingClientRect();
     // 计算移动的距离
     const x = position.x - originPosition.left;
     const y = position.y - originPosition.top;
 
-    projectStore.updateElement(componentData.key, {
-      position: {
-        x: componentData.position.x + x,
-        y: componentData.position.y + y,
-      },
+    projectStore.updateElement(componentData.key, "position", {
+      x: componentData.position.x + x,
+      y: componentData.position.y + y,
     });
   },
   onStart: () => {
     projectStore.setSelectedElementKeys([componentData.key]);
-  }
+  },
 });
 const handleSelectComponent = () => {
   projectStore.setSelectedElementKeys([componentData.key]);
@@ -115,7 +114,7 @@ const startPoint = {
 const handleDragPoint = (type: string, e: PointerEvent) => {
   const moveX = (e.x - startPoint.x) / stageStore.scale;
   const moveY = (e.y - startPoint.y) / stageStore.scale;
-  
+
   let width = componentData.props.width;
   let height = componentData.props.height;
   let x = componentData.position.x;
@@ -161,16 +160,11 @@ const handleDragPoint = (type: string, e: PointerEvent) => {
   startPoint.x = e.x;
   startPoint.y = e.y;
 
-  if(width < 10 || height < 10) return;
+  if (width < 10 || height < 10) return;
 
-  projectStore.updateElement(componentData.key, {
-    position: { x, y },
-    props: { 
-      ...componentData.props,
-      width,
-      height,
-     },
-  });
+  projectStore.updateElement(componentData.key, "position", { x, y });
+  projectStore.updateElement(componentData.key, "props.width", width);
+  projectStore.updateElement(componentData.key, "props.height", height);
 };
 // 拖拽点开始
 const handleDragStart = (_, e: PointerEvent) => {
@@ -206,6 +200,15 @@ const handleDragEnd = () => {
     border-radius: 50%;
     border: solid 1px @primary-color;
   }
+  .name-tip {
+    position: absolute;
+    top: -20px;
+    left: 4px;
+    font-size: 12px;
+    color: #fff;
+    background: @primary-color;
+    padding: 2px 4px;
+  }
   .top-left {
     top: -4px;
     left: -4px;

+ 43 - 38
src/views/designer/component/LayerItem.vue

@@ -1,20 +1,21 @@
 <template>
-  <div class="list-item" 
-    :class="{ 'list-item-active': layerData.active }"
+  <div
+    class="list-item"
+    :class="{
+      'list-item-active': projectStore.selectedElementKeys.includes(data.key),
+    }"
     @mouseenter="isHover = true"
     @mouseleave="isHover = false"
     @click="handleActive"
   >
     <template v-if="!isEditing">
       <span class="list-item-visible">
-        <span v-show="isHover">
-          <EyeOutlined v-if="true" />
-          <EyeInvisibleOutlined v-else />
-        </span>
+        <EyeOutlined v-if="data.visible && isHover" @click="handleVisible(false)" />
+        <EyeInvisibleOutlined v-if="!data.visible" @click="handleVisible(true)" />
       </span>
       <span class="layer-name">
         <Tooltip :title="data.name">
-          {{ data.name }}
+          <span @dblclick="isEditing = true">{{ data.name }}</span>
         </Tooltip>
       </span>
       <span class="layer-action">
@@ -30,29 +31,34 @@
               </Menu>
             </template>
           </Dropdown>
+        </span>
 
-          <Tooltip title="锁定" v-if="true">
-            <LockOutlined />
-          </Tooltip>
-          <Tooltip title="解锁" v-else>
-            <UnlockOutlined />
+        <Tooltip title="解锁" v-if="data.locked">
+          <LockOutlined @click="handleLock(false)"/>
+        </Tooltip>
+        <span v-else v-show="isHover">
+          <Tooltip title="锁定">
+            <UnlockOutlined @click="handleLock(true)"/>
           </Tooltip>
         </span>
+        
       </span>
     </template>
 
-    <Input 
+    <Input
       v-else
-      v-model:value="layerData.name"
+      v-model:value="layerName"
       placeholder="请输入图层名称"
       size="small"
+      :status="!layerName ? 'error' : undefined"
       @blur="handleChangeName"
     />
   </div>
 </template>
 
 <script setup lang="ts">
-import { defineProps, defineEmits, computed, ref } from "vue";
+import type { CustomElement } from "#/project";
+import { defineProps, ref } from "vue";
 import { Tooltip, Dropdown, Menu, MenuItem, Input } from "ant-design-vue";
 import {
   EyeOutlined,
@@ -63,48 +69,46 @@ import {
   EditOutlined,
   DeleteOutlined,
 } from "@ant-design/icons-vue";
-
-interface LayerItemData {
-  // 图层名称
-  name: string;
-  // 图层id
-  id: number;
-  // 是否可见
-  visible: boolean;
-  // 是否锁定
-  lock: boolean;
-  // 是否激活
-  active: boolean;
-}
+import { useProjectStore } from "@/store/modules/project";
 
 const props = defineProps<{
-  data: LayerItemData;
+  data: CustomElement;
 }>();
 
-const emit = defineEmits(["change", "delete"]);
+const projectStore = useProjectStore();
 
-const layerData = computed({
-  get: () => props.data,
-  set: (value: LayerItemData) => emit("change", value),
-})
+const layerName = ref<string>(props.data.name);
 
 const isEditing = ref(false);
 const isHover = ref(false);
 
-const handleMenuClick = ({key}: {key: 'rename' | 'del'}) => {
-  if(key === 'rename') {
+const handleMenuClick = ({ key }: { key: "rename" | "del" }) => {
+  if (key === "rename") {
     isEditing.value = true;
   } else {
-    emit("delete", props.data.id);
+    projectStore.removeElement(props.data.key);
   }
 };
 
 const handleChangeName = () => {
   isEditing.value = false;
+  if (!layerName.value) {
+    layerName.value = props.data.name;
+    return;
+  };
+  projectStore.updateElement(props.data.key, "name", layerName.value);
 };
 
 const handleActive = () => {
-  layerData.value.active = !layerData.value.active;
+  projectStore.setSelectedElementKeys([props.data.key]);
+};
+
+const handleLock = (locked: boolean) => {
+  projectStore.updateElement(props.data.key, "locked", locked);
+};
+
+const handleVisible = (visible: boolean) => {
+  projectStore.updateElement(props.data.key, "visible", visible);
 };
 </script>
 
@@ -138,6 +142,7 @@ const handleActive = () => {
   }
   .layer-action {
     width: 30px;
+    text-align: right;
   }
 }
 </style>

+ 44 - 10
src/views/designer/component/LayerManagement.vue

@@ -7,7 +7,12 @@
       </Button>
     </div>
 
-    <InputSearch allowClear size="small" placeholder="请输入图层名称" />
+    <InputSearch
+      allowClear
+      size="small"
+      placeholder="请输入图层名称"
+      @search="handleFilterLayer"
+    />
 
     <div class="layer-list">
       <VueDraggable
@@ -24,27 +29,56 @@
         </template>
       </VueDraggable>
 
-      <Empty v-else description="暂无图层" />
+      <Empty
+        v-else
+        description="暂无图层"
+        :image="Empty.PRESENTED_IMAGE_SIMPLE"
+        :style="{ marginTop: '100px' }"
+      />
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref } from "vue";
+import type { CustomElement } from "#/project";
+import { ref, watch } from "vue";
 import { Button, InputSearch, Empty } from "ant-design-vue";
 import { CloseOutlined } from "@ant-design/icons-vue";
 import VueDraggable from "vuedraggable";
 import LayerItem from "./LayerItem.vue";
+import { useProjectStore } from "@/store/modules/project";
 
-const layerList = ref(
-  Array.from({ length: 10 }, (_, index) => ({
-    name: `图层${index + 1}`,
-    id: index,
-  }))
+const projectStore = useProjectStore();
+const filter = ref<string>("");
+const layerList = ref<CustomElement[]>([]);
+
+watch(
+  () => [
+    projectStore.elements,
+    filter.value,
+  ],
+  () => {
+    const list = projectStore.elements.filter((item) =>
+      item.name.includes(filter.value)
+    );
+    layerList.value = list.sort((a, b) => b.zIndex - a.zIndex);
+  },
+  {
+    immediate: true,
+    deep: true
+  }
 );
 
-const dragEnd = (event: DragEvent) => {
-  console.log("dragEnd", event, layerList.value);
+const handleFilterLayer = (value: string) => {
+  filter.value = value;
+};
+
+const dragEnd = (event: CustomEvent & {newIndex: number}) => {
+  const length = layerList.value.length;
+  layerList.value.forEach((item, index) => {
+    projectStore.updateElement(item.key, "zIndex", length - index);
+  });
+  projectStore.setSelectedElementKeys([layerList.value[event.newIndex].key]);
 };
 </script>
 

+ 3 - 1
src/views/designer/component/Stage.vue

@@ -15,9 +15,11 @@
         @click="handleCanvasClick"
       >
         <ComponentWrapper 
-          v-for="item in projectStore.elements" 
+          v-for="item in projectStore.elements"
+          v-show="item.visible"
           :component-data="item"
           :key="item.key"
+          :style="{zIndex: item.zIndex}"
         />
       </div>
     </div>

+ 3 - 1
types/index.d.ts

@@ -1,3 +1,5 @@
 declare interface Fn<T = any, R = T> {
   (...arg: T[]): R;
-}
+}
+
+declare module 'lodash';

+ 9 - 10
types/project.d.ts

@@ -1,5 +1,5 @@
 import type { ComponentType } from "@/components";
-interface BackgroundOptions {
+declare interface BackgroundOptions {
   // 背景类型
   type: 'color' | 'image';
   // 背景颜色
@@ -10,7 +10,7 @@ interface BackgroundOptions {
   fillType?: string;
 }
 
-interface CustomElement {
+declare interface CustomElement {
   // 元素唯一标识
   key: number;
   // 元素名称
@@ -22,13 +22,12 @@ interface CustomElement {
     x: number;
     y: number;
   };
-  // 元素尺寸
-  size: {
-    width: number;
-    height: number;
-  };
   // 元素层级
   zIndex: number;
+  // 是否可见
+  visible: boolean;
+  // 是否锁定
+  locked: boolean;
   // 元素属性 -- 包含样式,组件属性
   props: Record<string, any>;
   // 元素交互
@@ -39,7 +38,7 @@ interface CustomElement {
   content: Record<string, any>;
 }
 
-export interface ReferLine {
+declare export interface ReferLine {
   // 辅助线唯一标识
   key: number;
   // 辅助线类型
@@ -52,7 +51,7 @@ export interface ReferLine {
   y?: number;
 }
 
-interface Page {
+declare interface Page {
   // 页面名称
   name: string;
   // 页面背景
@@ -64,7 +63,7 @@ interface Page {
 }
 
 // 项目基本信息
-export interface ProjectInfo {
+declare export interface ProjectInfo {
   // 项目名称
   name: string;
   // 项目描述