Jelajahi Sumber

private graphics

Alsmile 2 tahun lalu
induk
melakukan
95e1360adf

+ 73 - 0
src/styles/tdesign.css

@@ -464,6 +464,9 @@
 
   .t-dialog__header {
     font-size: 14px;
+    .t-icon {
+      width: 18px;
+    }
   }
 
   .t-dialog__body {
@@ -516,3 +519,73 @@
     background: none;
   }
 }
+
+.context-menu {
+  height: fit-content !important;
+  position: absolute;
+  border-radius: 4px;
+  background-color: var(--color-background-popup);
+  user-select: none;
+  z-index: 100;
+
+  .t-menu {
+    padding: 8px 4px;
+
+    .t-menu__item {
+      color: var(--color);
+      line-height: 32px;
+      padding: 0 16px;
+      margin: 0;
+
+      &:hover:not(.t-is-active):not(.t-is-opened):not(.t-is-disabled) {
+        background-color: var(--color-background-popup-hover);
+      }
+
+      &.t-is-disabled {
+        color: var(--color-gray);
+      }
+
+      .t-menu__content {
+        width: 100%;
+      }
+
+      & > div {
+        display: none;
+      }
+    }
+
+    .t-divider {
+      margin: 4px 0;
+      width: 100%;
+      border-top: 1px solid var(--color-border-input);
+    }
+  }
+}
+
+.t-menu__popup {
+  ul {
+    padding: 8px 4px;
+  }
+  .t-menu__item {
+    line-height: 32px;
+    height: 32px;
+    margin: 0;
+
+    & > div {
+      display: none;
+    }
+
+    .t-menu__content {
+      color: var(--color);
+      padding: 4px 0;
+    }
+
+    &:hover:not(.t-is-active):not(.t-is-opened):not(.t-is-disabled) {
+      background-color: var(--color-background-popup-hover);
+    }
+
+    &.t-is-disabled {
+      color: var(--color-gray);
+    }
+  }
+}

+ 1 - 1
src/views/Index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="page-app">
+  <div class="page-app" @contextmenu.prevent>
     <Header />
 
     <div class="design-body">

+ 131 - 352
src/views/components/ContextMenu.vue

@@ -1,389 +1,168 @@
 <template>
-  <div class="l-context-menu t-dropdown__menu">
+  <t-menu class="context-menu" @change="onMenu">
     <template v-if="props.type === 'anchor'">
-      <t-dropdown-item>
-        <a @click="onAddAnchorHand">
-          <div class="flex">添加手柄 <span class="flex-grow"></span> H</div>
-        </a>
-      </t-dropdown-item>
-      <t-dropdown-item>
-        <a @click="onRemoveAnchorHand">
-          <div class="flex">删除手柄 <span class="flex-grow"></span> D</div>
-        </a>
-      </t-dropdown-item>
-      <t-dropdown-item>
-        <a @click="onToggleAnchorHand">
-          <div class="flex">切换手柄 <span class="flex-grow"></span> Shift</div>
-        </a>
-      </t-dropdown-item>
+      <t-menu-item value="addAnchorHand">
+        <div class="flex">添加手柄 <span class="flex-grow"></span> H</div>
+      </t-menu-item>
+      <t-menu-item value="removeAnchorHand">
+        <div class="flex">删除手柄 <span class="flex-grow"></span> D</div>
+      </t-menu-item>
+      <t-menu-item value="toggleAnchorHand">
+        <div class="flex">切换手柄 <span class="flex-grow"></span> Shift</div>
+      </t-menu-item>
     </template>
     <template v-if="props.type === 'pen'">
-      <t-dropdown-item
-        :class="!choosePens() && !choosePen() ? 'item-diabled' : ''"
-      >
-        <a @click="layerMove('top')">
-          <div class="flex">置顶 <span class="flex-grow"></span></div>
-        </a>
-      </t-dropdown-item>
-      <t-dropdown-item
-        :class="!choosePens() && !choosePen() ? 'item-diabled' : ''"
-      >
-        <a @click="layerMove('bottom')">
-          <div class="flex">置底 <span class="flex-grow"></span></div>
-        </a>
-      </t-dropdown-item>
-      <t-dropdown-item
-        :class="!choosePens() && !choosePen() ? 'item-diabled' : ''"
-      >
-        <a @click="layerMove('up')">
-          <div class="flex">上一个图层 <span class="flex-grow"></span></div>
-        </a>
-      </t-dropdown-item>
-      <t-dropdown-item
-        :class="!choosePens() && !choosePen() ? 'item-diabled' : ''"
-      >
-        <a @click="layerMove('down')">
-          <div class="flex">下一个图层 <span class="flex-grow"></span></div>
-        </a>
-      </t-dropdown-item>
+      <t-menu-item :disabled="!selections.mode" value="top"> 置顶 </t-menu-item>
+      <t-menu-item :disabled="!selections.mode" value="bottom">
+        置底
+      </t-menu-item>
+      <t-menu-item :disabled="!selections.mode" value="up">
+        上一个图层
+      </t-menu-item>
+      <t-menu-item :disabled="!selections.mode" value="down">
+        下一个图层
+      </t-menu-item>
       <t-divider />
-      <t-dropdown-item
-        :class="!choosePens() && !choosePen() ? 'item-diabled' : ''"
-      >
-        <a @click="layerMove('down')">
-          <div class="flex">下一个图层 <span class="flex-grow"></span></div>
-        </a>
-      </t-dropdown-item>
-      <template v-if="choosePens()">
-        <t-dropdown-item>
-          <a @click="combine()">
-            <div class="flex">组合 <span class="flex-grow"></span></div>
-          </a>
-        </t-dropdown-item>
-        <t-dropdown-item>
-          <a @click="combine(0)">
-            <div class="flex">组合为状态 <span class="flex-grow"></span></div>
-          </a>
-        </t-dropdown-item>
+
+      <template v-if="selections.mode === SelectionMode.Pens">
+        <t-menu-item value="group"> 组合 </t-menu-item>
+        <t-menu-item value="states"> 组合为状态 </t-menu-item>
       </template>
-      <t-dropdown-item v-if="hasChildren()">
-        <a @click="unCombine()">
-          <div class="flex">取消组合 <span class="flex-grow"></span></div>
-        </a>
-      </t-dropdown-item>
-      <t-dropdown-item
-        :class="!choosePens() && !choosePen() ? 'item-diabled' : ''"
-      >
-        <a @click="onLock">
-          <div class="flex">
-            {{ getLocked() ? '解锁' : '锁定' }} <span class="flex-grow"></span>
-          </div>
-        </a>
-      </t-dropdown-item>
+      <t-menu-item v-if="hasChildren" value="unGroup"> 取消组合 </t-menu-item>
+      <t-menu-item :disabled="!selections.mode" value="lock">
+        {{ locked ? '解锁' : '锁定' }}
+      </t-menu-item>
       <t-divider />
-      <t-dropdown-item
-        :class="!choosePens() && !choosePen() ? 'item-diabled' : ''"
-      >
-        <a @click="onDel">
-          <div class="flex">删除<span class="flex-grow"></span></div>
-        </a>
-      </t-dropdown-item>
-      <template v-if="isLine()">
-        <t-dropdown-item v-if="isLineType()">
-          <a @click="changeType(0)">
-            <div class="flex">变成节点<span class="flex-grow"></span></div>
-          </a>
-        </t-dropdown-item>
-        <t-dropdown-item v-else>
-          <a @click="changeType(1)">
-            <div class="flex">变成连线<span class="flex-grow"></span></div>
-          </a>
-        </t-dropdown-item>
+      <t-menu-item :disabled="!selections.mode" value="del"> 删除 </t-menu-item>
+      <template v-if="isNameLine">
+        <t-menu-item value="changeType">
+          {{ isLine ? '变成节点' : '变成连线' }}
+        </t-menu-item>
       </template>
       <t-divider />
-      <t-dropdown-item :class="cantUndo() ? 'item-diabled' : ''">
-        <a @click="onUndo">
-          <div class="flex">撤销<span class="flex-grow"></span>Ctrl + Z</div>
-        </a>
-      </t-dropdown-item>
-      <t-dropdown-item :class="cantRedo() ? 'item-diabled' : ''">
-        <a @click="onRedo">
-          <div class="flex">恢复<span class="flex-grow"></span>Shift + Z</div>
-        </a>
-      </t-dropdown-item>
+      <t-menu-item :disabled="cantUndo" value="undo">
+        <div class="flex">撤销<span class="flex-grow"></span>Ctrl + Z</div>
+      </t-menu-item>
+      <t-menu-item :disabled="cantRedo" value="redo">
+        <div class="flex">恢复<span class="flex-grow"></span>Shift + Z</div>
+      </t-menu-item>
       <t-divider />
-      <t-dropdown-item
-        :class="!choosePens() && !choosePen() ? 'item-diabled' : ''"
-      >
-        <a @click="onCut">
-          <div class="flex">剪切<span class="flex-grow"></span>Ctrl + X</div>
-        </a>
-      </t-dropdown-item>
-      <t-dropdown-item
-        :class="!choosePens() && !choosePen() ? 'item-diabled' : ''"
-      >
-        <a @click="onCopy">
-          <div class="flex">复制<span class="flex-grow"></span>Ctrl + C</div>
-        </a>
-      </t-dropdown-item>
-      <t-dropdown-item>
-        <a @click="onPaste">
-          <div class="flex">粘贴<span class="flex-grow"></span>Ctrl + V</div>
-        </a>
-      </t-dropdown-item>
+      <t-menu-item :disabled="!selections.mode" value="cut">
+        <div class="flex">剪切<span class="flex-grow"></span>Ctrl + X</div>
+      </t-menu-item>
+      <t-menu-item :disabled="!selections.mode" value="copy">
+        <div class="flex">复制<span class="flex-grow"></span>Ctrl + C</div>
+      </t-menu-item>
+      <t-menu-item value="paste">
+        <div class="flex">粘贴<span class="flex-grow"></span>Ctrl + V</div>
+      </t-menu-item>
     </template>
-  </div>
+  </t-menu>
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue';
-import { LockState, Pen, PenType, Meta2d } from '@meta2d/core';
+import { onMounted, ref } from 'vue';
+import { LockState, Pen, PenType } from '@meta2d/core';
+import { useSelection, SelectionMode } from '@/services/selections';
 
 const props = defineProps({
   type: String,
 });
 const emit = defineEmits(['changeVisible']);
-function closeMenu() {
-  emit('changeVisible', false);
-}
-const onAddAnchorHand = () => {
-  meta2d.addAnchorHand();
-  closeMenu();
-};
 
-const onRemoveAnchorHand = () => {
-  meta2d.removeAnchorHand();
-  closeMenu();
-};
-
-const onToggleAnchorHand = () => {
-  meta2d.toggleAnchorHand();
-  closeMenu();
-};
+const { selections } = useSelection();
 
-function choosePen(): boolean {
-  return meta2d?.store.active?.length === 1;
-}
+const hasChildren = ref(false);
+const locked = ref(false);
+const isNameLine = ref(false);
+const isLine = ref(false);
+const cantUndo = ref(false);
+const cantRedo = ref(false);
 
-function choosePens(): boolean {
-  return meta2d?.store.active?.length > 1;
-}
-
-function layerMove(type: 'top' | 'bottom' | 'up' | 'down') {
-  const pens = meta2d.store.active;
-  if (hasImage()) {
-    if (type === 'top') {
-      meta2d.setValue({
-        id: pens[0].id,
-        isBottom: false,
-      });
-      meta2d.top(pens[0]);
-    } else if (type === 'bottom') {
-      meta2d.setValue({
-        id: pens[0].id,
-        isBottom: true,
-      });
-      meta2d.bottom(pens[0]);
-    } else if (type === 'up') {
-      if (pens[0].isBottom) {
-        meta2d.setValue({
-          id: pens[0].id,
-          isBottom: false,
-        });
-      } else {
-        meta2d.up(pens[0]);
-      }
-    } else if (type === 'down') {
-      if (!pens[0].isBottom) {
-        meta2d.setValue({
-          id: pens[0].id,
-          isBottom: true,
-        });
-      } else {
-        meta2d.down(pens[0]);
-      }
-    }
-  } else {
-    if (pens[0].name === 'gif') {
-      let zIndex = pens[0].calculative.zIndex;
-      if (type === 'top') {
-        zIndex == 999;
-      }
-      if (type === 'bottom') {
-        zIndex == -999;
-      }
-      if (type === 'up') {
-        zIndex++;
-      }
-      if (type === 'down') {
-        zIndex--;
-      }
-      meta2d.setValue({
-        id: pens[0].id,
-        zIndex,
-      });
-    } else {
-      if (Array.isArray(pens)) {
-        for (const pen of pens) {
-          meta2d[type](pen);
-        }
-      }
-    }
-    meta2d.render();
+onMounted(() => {
+  if (selections.mode === SelectionMode.Pen) {
+    hasChildren.value = meta2d?.store.active[0]?.children?.length > 0;
+    isNameLine.value = meta2d?.store.active[0]?.name === 'line';
+    isLine.value = meta2d?.store.active[0]?.type === PenType.Line;
   }
-  emit('changeVisible', false);
-}
-function hasChildren(): boolean {
-  return choosePen() && meta2d?.store.active[0]?.children?.length > 0;
-}
 
-function getLocked() {
-  return meta2d?.store.active?.some((pen: Pen) => pen.locked);
-}
-
-function combine(showChild?: number) {
-  meta2d.combine(meta2d.store.active, showChild);
-  closeMenu();
-}
-
-function unCombine() {
-  meta2d.uncombine();
-  closeMenu();
-}
-
-function onLock() {
-  const locked = !getLocked();
-  const pens = meta2d.store.active;
-  if (Array.isArray(pens)) {
-    for (const pen of pens) {
-      pen.locked = locked ? LockState.DisableMove : LockState.None;
-    }
+  if (selections.mode) {
+    locked.value = meta2d?.store.active?.some((pen: Pen) => pen.locked);
   }
-  meta2d.render();
-  closeMenu();
-}
-
-function onDel() {
-  meta2d.delete();
-  closeMenu();
-}
-
-function onUndo() {
-  meta2d.undo();
-  closeMenu();
-}
 
-function onRedo() {
-  meta2d.redo();
-  closeMenu();
-}
-
-function onCut() {
-  meta2d.cut();
-  closeMenu();
-}
-
-function onCopy() {
-  meta2d.copy();
-  closeMenu();
-}
-
-function onPaste() {
-  meta2d.paste();
-  closeMenu();
-}
-
-function cantUndo(): boolean {
-  return (
+  cantUndo.value =
     !!meta2d?.store.data.locked ||
     meta2d?.store.histories.length == 0 ||
     meta2d?.store.historyIndex == null ||
-    meta2d?.store.historyIndex < 0
-  );
-}
+    meta2d?.store.historyIndex < 0;
 
-function cantRedo(): boolean {
-  return (
+  cantRedo.value =
     !!meta2d?.store.data.locked ||
     meta2d?.store.histories.length == 0 ||
     meta2d?.store.historyIndex == null ||
-    meta2d?.store.historyIndex > meta2d?.store.histories.length - 2
-  );
-}
-
-function isLine(): boolean {
-  return choosePen() && meta2d?.store.active[0]?.name === 'line';
-}
-
-function isLineType(): boolean {
-  return meta2d?.store.active[0]?.type === PenType.Line;
-}
-
-function changeType(type: number) {
-  const id = meta2d.store.active[0].id;
-  meta2d.setValue(
-    {
-      id,
-      type,
-    },
-    {
-      history: true,
-    }
-  );
-  closeMenu();
-}
-
-function hasImage(): boolean {
-  const pen = meta2d.store.active[0];
-  return choosePen() && pen.image && pen.name !== 'gif';
-}
-</script>
-
-<style lang="postcss" scoped>
-.l-context-menu {
-  position: absolute;
-  display: flex;
-  flex-direction: column;
-  padding: 6px;
-  border-radius: 6px;
-  gap: 2px;
-  background-color: var(--color-background-popup);
-  z-index: 999;
-  .t-dropdown__item {
-    max-width: 240px !important;
-    /* padding: 0;
-    background-color: transparent; */
-
-    &:hover {
-      background-color: var(--color-background-popup-hover);
-    }
-
-    a {
-      width: 150px;
-      display: block;
-      text-decoration: none;
-      color: var(--color);
-      margin-left: 8px;
+    meta2d?.store.historyIndex > meta2d?.store.histories.length - 2;
+});
 
-      label {
-        font-size: 10px;
-        background-color: #ff400030;
-        color: var(--color-bland);
-        padding: 0 6px;
-        margin-left: 4px;
-        border-radius: 2px;
+const onMenu = (val: string) => {
+  switch (val) {
+    case 'addAnchorHand':
+      meta2d.addAnchorHand();
+      break;
+    case 'removeAnchorHand':
+      meta2d.removeAnchorHand();
+      break;
+    case 'toggleAnchorHand':
+      meta2d.toggleAnchorHand();
+      break;
+    case 'top':
+    case 'bottom':
+    case 'up':
+    case 'down':
+    case 'undo':
+    case 'redo':
+    case 'cut':
+    case 'copy':
+    case 'paste':
+      meta2d[val]();
+      break;
+    case 'group':
+      meta2d.combine(meta2d.store.active);
+      break;
+    case 'states':
+      meta2d.combine(meta2d.store.active, 0);
+      break;
+    case 'unGroup':
+      meta2d.uncombine();
+      break;
+    case 'lock':
+      {
+        const pens = meta2d.store.active;
+        if (Array.isArray(pens)) {
+          for (const pen of pens) {
+            pen.locked = locked.value ? LockState.DisableMove : LockState.None;
+          }
+        }
+        meta2d.render();
       }
-    }
+      break;
+    case 'del':
+      meta2d.delete();
+      break;
+    case 'changeType':
+      meta2d.setValue(
+        {
+          id: meta2d.store.active[0].id,
+          type: isLine ? 0 : 1,
+        },
+        {
+          history: true,
+        }
+      );
+      break;
   }
 
-  .item-diabled {
-    a {
-      cursor: not-allowed;
-      color: var(--td-text-color-disabled);
-    }
-    &:hover {
-      background-color: #0000;
-    }
-  }
-}
-</style>
+  emit('changeVisible', false);
+};
+</script>
+
+<style lang="postcss" scoped></style>

+ 147 - 10
src/views/components/Graphics.vue

@@ -32,15 +32,16 @@
             :key="item.name"
           >
             <template #header>
-              <div class="flex middle mr-8" @click.stop>
-                <t-input
-                  v-if="item.edited"
-                  v-model="item.label"
-                  style="width: 140px"
-                  @blur="createFoder"
-                  @enter="createFoder"
-                  @keyup="onKeyHeader"
-                />
+              <div class="flex middle mr-8">
+                <div v-if="item.edited" @click.stop>
+                  <t-input
+                    v-model="item.label"
+                    style="width: 140px"
+                    @blur="createFoder"
+                    @enter="createFoder"
+                    @keyup="onKeyHeader"
+                  />
+                </div>
                 <div v-else class="ellipsis" style="width: 140px">
                   {{ item.name }}
                 </div>
@@ -80,6 +81,7 @@
                 @dragend="dragEnd()"
                 @click.stop="dragStart($event, elem)"
                 @dblclick.stop="open(elem)"
+                @contextmenu="onContextMenu($event, item, elem)"
                 :title="elem.draggable === false ? '双击打开' : '拖拽到画布'"
               >
                 <t-image
@@ -142,11 +144,48 @@
         </div>
       </div>
     </div>
+
+    <div
+      class="context-menu-box"
+      ref="contextmenuDom"
+      v-if="contextmenu.visible"
+      tabindex="0"
+      :style="contextmenu.style"
+      @blu1r="contextmenu.visible = false"
+    >
+      <t-menu class="context-menu" @change="onMenu" expandType="popup">
+        <t-submenu
+          value="move"
+          title="移动到"
+          v-if="contextmenu.subMenus.length"
+        >
+          <t-menu-item
+            v-for="subMenu in contextmenu.subMenus"
+            :value="'move:' + subMenu.name"
+          >
+            {{ subMenu.name }}
+          </t-menu-item>
+        </t-submenu>
+        <t-menu-item value="edit"> 编辑 </t-menu-item>
+        <t-menu-item value="del"> 删除 </t-menu-item>
+      </t-menu>
+    </div>
+
+    <t-dialog
+      v-if="delDialog.show"
+      theme="danger"
+      header="删除"
+      :visible="true"
+      @close="delDialog.show = false"
+      @confirm="delComponet"
+    >
+      确定删除该数据吗?删除后不可恢复!
+    </t-dialog>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
+import { onMounted, onUnmounted, reactive, ref } from 'vue';
 import { useRouter } from 'vue-router';
 import axios from 'axios';
 
@@ -592,6 +631,79 @@ const onKeyHeader = (text: string, event: any) => {
   }
 };
 
+const contextmenu = reactive<any>({
+  visible: false,
+  style: {},
+  subMenus: [],
+});
+const contextmenuDom = ref<any>(null);
+const onContextMenu = async (e: MouseEvent, group: string, item: any) => {
+  e.preventDefault();
+  e.stopPropagation();
+
+  if (activedGroup.value !== '我的' || !item.component) {
+    return;
+  }
+
+  contextmenu.group = group;
+  contextmenu.component = item;
+
+  if (document.body.clientHeight - e.clientY < 128) {
+    contextmenu.style = {
+      left: e.clientX + 'px',
+      bottom: '4px',
+    };
+  } else {
+    contextmenu.style = {
+      left: e.clientX + 'px',
+      top: e.clientY + 'px',
+    };
+  }
+
+  contextmenu.subMenus = [];
+  for (const elem of subGroups.value) {
+    if (elem === group || elem.name === '3D') {
+      continue;
+    }
+    contextmenu.subMenus.push(elem);
+  }
+  contextmenu.visible = true;
+  setTimeout(() => {
+    if (contextmenuDom.value) {
+      contextmenuDom.value.focus();
+    }
+  }, 500);
+};
+
+const delDialog = reactive<any>({});
+
+const onMenu = async (val: string) => {
+  switch (val) {
+    case 'edit':
+      const id = contextmenu.component._id || contextmenu.component.id;
+      autoSave();
+      router.push({
+        path: '/',
+        query: {
+          id,
+          c: 1,
+          r: Date.now() + '',
+        },
+      });
+
+      break;
+    case 'del':
+      delDialog.show = true;
+      break;
+  }
+
+  contextmenu.visible = false;
+};
+
+const delComponet = () => {
+  delDialog.show = false;
+};
+
 onMounted(() => {
   groupChange('场景');
   document.addEventListener('dragstart', dragstart, false);
@@ -620,6 +732,7 @@ onUnmounted(() => {
     flex-grow: 1;
     overflow-y: auto;
     font-size: 12px;
+    z-index: 100;
 
     .groups {
       & > div {
@@ -786,5 +899,29 @@ onUnmounted(() => {
       }
     }
   }
+
+  .context-menu-box {
+    position: fixed;
+    z-index: 200;
+    & > div {
+      width: 140px !important;
+    }
+
+    :deep(.t-menu) {
+      .t-menu__item {
+        &.t-is-opened {
+          background-color: var(--color-background-popup-hover);
+          transition: none !important;
+        }
+      }
+      .t-fake-arrow {
+        transform: rotate(-90deg) !important;
+      }
+
+      .t-fake-arrow--active {
+        transform: rotate(90deg) !important;
+      }
+    }
+  }
 }
 </style>

+ 1 - 0
src/views/components/Header.vue

@@ -9,6 +9,7 @@
       :maxHeight="560"
       :delay2="[10, 150]"
       overlayClassName="header-dropdown"
+      trigger1="click"
     >
       <a> 文件 </a>
       <t-dropdown-menu>

+ 68 - 71
src/views/components/View.vue

@@ -7,29 +7,28 @@
       <t-tooltip content="保存为大屏" placement="bottom">
         <a>
           <t-badge
-            :style="{
-              color: canSave ? '' : '#4f5b75',
-              cursor: canSave ? '' : 'not-allowed',
-            }"
+            :class="{ gray: route.query.c }"
             dot
             :showZero="false"
             :count="dot ? 1 : 0"
           >
             <t-icon
               name="save"
-              @click="canSave && save(SaveType.Save, false, true)" /></t-badge
-        ></a>
+              @click="!route.query.c && save(SaveType.Save, false, true)"
+            />
+          </t-badge>
+        </a>
       </t-tooltip>
       <t-tooltip content="保存为我的组件" placement="bottom">
-        <a
-          :style="{
-            color: canSaveComponent ? '' : '#4f5b75',
-            cursor: canSaveComponent ? '' : 'not-allowed',
-          }"
-          ><t-icon
+        <a :class="{ gray: route.query.id && !route.query.c }">
+          <t-icon
             name="layers"
-            @click="canSaveComponent && save(SaveType.Save, true, true)"
-        /></a>
+            @click="
+              (!route.query.id || route.query.c) &&
+                save(SaveType.Save, true, true)
+            "
+          />
+        </a>
       </t-tooltip>
       <t-tooltip content="格式化(双击可连续使用)" placement="bottom">
         <a
@@ -248,10 +247,7 @@
         <a><t-icon name="qrcode" /></a>
         <template #content>
           <div style="padding: 12px 12px 6px 12px">
-            <img
-              v-if="route.query.id && route.query.component !== 'true'"
-              :src="qrcode.url"
-            />
+            <img v-if="route.query.id && !route.query.c" :src="qrcode.url" />
             <div
               v-else-if="!route.query.id"
               class="gray center mb-4"
@@ -277,12 +273,9 @@
     </div>
     <div id="meta2d"></div>
     <ContextMenu
-      :style="{
-        left: contextMenuValue.left,
-        top: contextMenuValue.top,
-      }"
-      v-show="contextMenuVisible"
-      :type="contextMenuType"
+      v-if="contextmenu.visible"
+      :type="contextmenu.type"
+      :style="contextmenu.style"
       @changeVisible="changeContextMenuVisible"
     />
     <t-dialog
@@ -726,7 +719,12 @@ import { MessagePlugin } from 'tdesign-vue-next';
 
 import { registerBasicDiagram } from '@/services/register';
 import { useUser } from '@/services/user';
-import { cdn, getLe5leV, updateCollection } from '@/services/api';
+import {
+  cdn,
+  getComponents,
+  getLe5leV,
+  updateCollection,
+} from '@/services/api';
 import {
   save,
   autoSave,
@@ -786,7 +784,6 @@ onMounted(() => {
   meta2d = new Meta2d('meta2d', meta2dOptions);
   registerBasicDiagram();
   open(true);
-  saveStatusChange();
   // @ts-ignore
   meta2d.on('active', active);
   // @ts-ignore
@@ -807,7 +804,7 @@ onMounted(() => {
   // 所有编辑栏所做修改
   meta2d.on('components-update-value', patchFlag);
   // @ts-ignore
-  meta2d.on('contextmenu', contextmenu);
+  meta2d.on('contextmenu', onContextmenu);
   meta2d.on('click', canvasClick);
 
   timer = setInterval(autoSave, 60000);
@@ -821,39 +818,27 @@ const watcher = watch(
   () => route.query,
   async () => {
     open();
-    saveStatusChange();
   }
 );
 
-const canSave = ref(true);
-const canSaveComponent = ref(true);
-
-const saveStatusChange = () => {
+const open = async (flag: boolean = false) => {
   if (route.query.id) {
-    if (route.query.component === 'true') {
-      canSave.value = false;
-    } else if (route.query.component === 'false') {
-      canSaveComponent.value = false;
+    let ret: any;
+    if (route.query.c) {
+      ret = await getComponents(route.query.id + '');
     } else {
-      canSaveComponent.value = false;
+      ret = await getLe5leV(route.query.id + '');
     }
-  } else {
-    canSave.value = true;
-    canSaveComponent.value = true;
-  }
-};
-
-const open = async (flag: boolean = false) => {
-  if (route.query.id) {
-    const ret: any = await getLe5leV(route.query.id + '');
     if (ret) {
       meta2d.open(ret);
-      shared.value = ret.shared;
+      if (!route.query.c) {
+        shared.value = ret.shared;
 
-      const qr: any = await QRCode.toDataURL(
-        `https://view2d.le5le.com/?id=${route.query.id + ''}`
-      );
-      qrcode.url = qr;
+        const qr: any = await QRCode.toDataURL(
+          `https://view2d.le5le.com/?id=${route.query.id + ''}`
+        );
+        qrcode.url = qr;
+      }
     }
   } else {
     if (flag) {
@@ -908,7 +893,7 @@ onUnmounted(() => {
     meta2d.off('translatePens', patchFlag);
     meta2d.off('components-update-value', patchFlag);
     // @ts-ignore
-    meta2d.off('contextmenu', contextmenu);
+    meta2d.off('contextmenu', onContextmenu);
     meta2d.off('click', canvasClick);
 
     meta2d.destroy();
@@ -1159,41 +1144,53 @@ const preview = async () => {
   });
 };
 
-const contextMenuVisible = ref(false);
-const contextMenuValue = ref({
-  left: '0px',
-  top: '0px',
+const contextmenu = reactive<any>({
+  visible: false,
+  type: '',
+  style: {},
 });
-const contextMenuType = ref('');
 
-const contextmenu = ({ e, rect }: { e: any; rect: any }) => {
-  contextMenuType.value = '';
-  contextMenuValue.value.left = e.clientX + 'px';
-  contextMenuValue.value.top = e.clientY + 'px';
+const onContextmenu = ({ e, rect }: { e: any; rect: any }) => {
+  contextmenu.type = '';
+  contextmenu.style = {
+    left: e.clientX + 'px',
+    top: e.clientY + 'px',
+  };
+
   if (
     meta2d.store.hoverAnchor &&
     meta2d.canvas.hoverType === HoverType.LineAnchor
   ) {
-    contextMenuType.value = 'anchor';
+    contextmenu.type = 'anchor';
+    if (document.body.clientHeight - e.clientY < 128) {
+      contextmenu.style = {
+        left: e.clientX + 'px',
+        bottom: '4px',
+      };
+    }
   } else {
-    //其他右键菜单
-    contextMenuType.value = 'pen';
+    contextmenu.type = 'pen';
+    if (document.body.clientHeight - e.clientY < 450) {
+      contextmenu.style = {
+        left: e.clientX + 'px',
+        bottom: '4px',
+      };
+    }
   }
 
-  if (!contextMenuVisible.value && contextMenuType.value) {
-    contextMenuVisible.value = true;
-  }
-  if (!contextMenuType.value) {
-    contextMenuVisible.value = false;
+  if (contextmenu.type) {
+    contextmenu.visible = true;
+  } else {
+    contextmenu.visible = false;
   }
 };
 
 const canvasClick = () => {
-  contextMenuVisible.value = false;
+  contextmenu.visible = false;
 };
 
 const changeContextMenuVisible = (e: boolean) => {
-  contextMenuVisible.value = e;
+  contextmenu.visible = e;
 };
 
 const networkColumns = ref([