mirror of
				https://github.com/Tencent/tmagic-editor.git
				synced 2025-11-04 10:49:51 +08:00 
			
		
		
		
	feat(editor): 支持 slide 侧边栏可拖拽悬浮
This commit is contained in:
		
							parent
							
								
									9098504e5f
								
							
						
					
					
						commit
						2b10e7eda9
					
				@ -61,6 +61,7 @@
 | 
			
		||||
    "keycon": "^1.4.0",
 | 
			
		||||
    "lodash-es": "^4.17.21",
 | 
			
		||||
    "monaco-editor": "^0.41.0",
 | 
			
		||||
    "moveable": "^0.51.1",
 | 
			
		||||
    "serialize-javascript": "^6.0.0",
 | 
			
		||||
    "vue": "^3.3.4"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <MFormDrawer
 | 
			
		||||
  <component
 | 
			
		||||
    :is="slideType === 'box' ? MFormBox : MFormDrawer"
 | 
			
		||||
    class="m-editor-code-block-editor"
 | 
			
		||||
    ref="fomDrawer"
 | 
			
		||||
    label-width="80px"
 | 
			
		||||
@ -19,7 +20,7 @@
 | 
			
		||||
    <template #left>
 | 
			
		||||
      <TMagicButton type="primary" text @click="difVisible = true">查看修改</TMagicButton>
 | 
			
		||||
    </template>
 | 
			
		||||
  </MFormDrawer>
 | 
			
		||||
  </component>
 | 
			
		||||
 | 
			
		||||
  <TMagicDialog v-model="difVisible" title="查看修改" fullscreen>
 | 
			
		||||
    <div style="display: flex; margin-bottom: 10px">
 | 
			
		||||
@ -50,11 +51,11 @@
 | 
			
		||||
import { computed, inject, onUnmounted, ref } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { TMagicButton, TMagicDialog, tMagicMessage, tMagicMessageBox, TMagicTag } from '@tmagic/design';
 | 
			
		||||
import { ColumnConfig, FormConfig, FormState, MFormDrawer } from '@tmagic/form';
 | 
			
		||||
import { ColumnConfig, FormConfig, FormState, MFormBox, MFormDrawer } from '@tmagic/form';
 | 
			
		||||
import type { CodeBlockContent } from '@tmagic/schema';
 | 
			
		||||
 | 
			
		||||
import CodeEditor from '@editor/layouts/CodeEditor.vue';
 | 
			
		||||
import type { Services } from '@editor/type';
 | 
			
		||||
import type { Services, SlideType } from '@editor/type';
 | 
			
		||||
import { getConfig } from '@editor/utils/config';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
@ -66,6 +67,7 @@ const props = defineProps<{
 | 
			
		||||
  disabled?: boolean;
 | 
			
		||||
  isDataSource?: boolean;
 | 
			
		||||
  dataSourceType?: string;
 | 
			
		||||
  slideType?: SlideType;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
 | 
			
		||||
@ -19,3 +19,4 @@
 | 
			
		||||
export * from './use-code-block-edit';
 | 
			
		||||
export * from './use-data-source-method';
 | 
			
		||||
export * from './use-stage';
 | 
			
		||||
export * from './use-float-box';
 | 
			
		||||
 | 
			
		||||
@ -68,6 +68,8 @@ export const useCodeBlockEdit = (codeBlockService?: CodeBlockService) => {
 | 
			
		||||
 | 
			
		||||
    await codeBlockService?.setCodeDslById(codeId.value, values);
 | 
			
		||||
 | 
			
		||||
    tMagicMessage.success('代码块保存成功');
 | 
			
		||||
 | 
			
		||||
    codeBlockEditor.value?.hide();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										153
									
								
								packages/editor/src/hooks/use-float-box.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								packages/editor/src/hooks/use-float-box.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,153 @@
 | 
			
		||||
import { computed, ComputedRef, inject, nextTick, ref, watch } from 'vue';
 | 
			
		||||
import Moveable from 'moveable';
 | 
			
		||||
 | 
			
		||||
import { type Services } from '@editor/type';
 | 
			
		||||
 | 
			
		||||
export const useFloatBox = (slideKeys: ComputedRef<string[]>) => {
 | 
			
		||||
  const services = inject<Services>('services');
 | 
			
		||||
  const moveable = ref<Moveable>();
 | 
			
		||||
  const floatBox = ref<HTMLElement[]>();
 | 
			
		||||
 | 
			
		||||
  const floatBoxStates = computed(() => services?.uiService.get('floatBox'));
 | 
			
		||||
 | 
			
		||||
  const curKey = ref('');
 | 
			
		||||
  const target = computed(() =>
 | 
			
		||||
    floatBox.value
 | 
			
		||||
      ? floatBox.value.find((item) => item.classList.contains(`m-editor-float-box-${curKey.value}`))
 | 
			
		||||
      : undefined,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const showingBoxKeys = computed(() =>
 | 
			
		||||
    [...(floatBoxStates.value?.keys() ?? [])].filter((key) => floatBoxStates.value?.get(key)?.status),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const isDraging = ref(false);
 | 
			
		||||
 | 
			
		||||
  const showFloatBox = async (key: string) => {
 | 
			
		||||
    const curBoxStatus = floatBoxStates.value?.get(curKey.value)?.status;
 | 
			
		||||
    if (curKey.value === key && curBoxStatus) return;
 | 
			
		||||
    curKey.value = key;
 | 
			
		||||
    setSlideState(key, {
 | 
			
		||||
      zIndex: getMaxZIndex() + 1,
 | 
			
		||||
      status: true,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await nextTick();
 | 
			
		||||
    if (moveable.value) {
 | 
			
		||||
      moveable.value.target = target.value;
 | 
			
		||||
      moveable.value.dragTarget = getDragTarget();
 | 
			
		||||
      moveable.value.updateRect();
 | 
			
		||||
    } else {
 | 
			
		||||
      initFloatBoxMoveable();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const setSlideState = (key: string, data: Record<string, string | number | boolean>) => {
 | 
			
		||||
    const slideState = floatBoxStates.value?.get(key);
 | 
			
		||||
    if (!slideState) return;
 | 
			
		||||
    floatBoxStates.value?.set(key, {
 | 
			
		||||
      ...slideState,
 | 
			
		||||
      ...data,
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getDragTarget = (key?: string) => `.m-editor-float-box-header-${key ?? curKey.value}`;
 | 
			
		||||
 | 
			
		||||
  const closeFloatBox = (key: string) => {
 | 
			
		||||
    setSlideState(key, {
 | 
			
		||||
      status: false,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 如果只有一个,关掉后需要销毁moveable实例
 | 
			
		||||
    if (!floatBoxStates.value?.values) return;
 | 
			
		||||
    const keys = [...floatBoxStates.value?.keys()];
 | 
			
		||||
    const values = [...floatBoxStates.value?.values()];
 | 
			
		||||
    const lastFloatBoxLen = values.filter((state) => state.status).length;
 | 
			
		||||
    if (lastFloatBoxLen === 0) {
 | 
			
		||||
      moveable.value?.destroy();
 | 
			
		||||
      moveable.value = undefined;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 如果关掉的 box 是最大的,需要选中下面的一层
 | 
			
		||||
    if (key === curKey.value) {
 | 
			
		||||
      // 查找显示的最大 zIndex 对应的 index
 | 
			
		||||
      const zIndexList = values.filter((item) => item.status).map((item) => item.zIndex);
 | 
			
		||||
      const maxZIndex = Math.max(...zIndexList);
 | 
			
		||||
      const key = keys.find((key) => floatBoxStates.value?.get(key)?.zIndex === maxZIndex);
 | 
			
		||||
      if (!key) return;
 | 
			
		||||
      showFloatBox(key);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getMaxZIndex = () => {
 | 
			
		||||
    if (!floatBoxStates.value?.values()) return 0;
 | 
			
		||||
    const list = [...floatBoxStates.value?.values()].map((state) => state.zIndex);
 | 
			
		||||
    return Math.max(...list) ?? 0;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const initFloatBoxMoveable = () => {
 | 
			
		||||
    const dragTarget = getDragTarget();
 | 
			
		||||
    moveable.value = new Moveable(document.body, {
 | 
			
		||||
      target: target.value,
 | 
			
		||||
      draggable: true,
 | 
			
		||||
      resizable: true,
 | 
			
		||||
      edge: true,
 | 
			
		||||
      keepRatio: false,
 | 
			
		||||
      origin: false,
 | 
			
		||||
      snappable: true,
 | 
			
		||||
      dragTarget,
 | 
			
		||||
      dragTargetSelf: false,
 | 
			
		||||
      linePadding: 10,
 | 
			
		||||
      controlPadding: 10,
 | 
			
		||||
      elementGuidelines: [...(floatBoxStates.value?.keys() ?? [])].map((key) => getDragTarget(key)),
 | 
			
		||||
      bounds: { left: 0, top: 0, right: 0, bottom: 0, position: 'css' },
 | 
			
		||||
    });
 | 
			
		||||
    moveable.value.on('drag', (e) => {
 | 
			
		||||
      e.target.style.transform = e.transform;
 | 
			
		||||
    });
 | 
			
		||||
    moveable.value.on('resize', (e) => {
 | 
			
		||||
      e.target.style.width = `${e.width}px`;
 | 
			
		||||
      e.target.style.height = `${e.height}px`;
 | 
			
		||||
      e.target.style.transform = e.drag.transform;
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const dragendHandler = (key: string, e: DragEvent) => {
 | 
			
		||||
    setSlideState(key, {
 | 
			
		||||
      left: e.clientX,
 | 
			
		||||
      top: e.clientY,
 | 
			
		||||
    });
 | 
			
		||||
    showFloatBox(key);
 | 
			
		||||
    isDraging.value = false;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  document.body.addEventListener('dragover', (e: DragEvent) => {
 | 
			
		||||
    if (!isDraging.value) return;
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const dragstartHandler = () => (isDraging.value = true);
 | 
			
		||||
 | 
			
		||||
  // 监听 slide 长度变化,更新 ui serice map
 | 
			
		||||
  watch(
 | 
			
		||||
    () => slideKeys.value,
 | 
			
		||||
    () => {
 | 
			
		||||
      services?.uiService.setFloatBox(slideKeys.value);
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      deep: true,
 | 
			
		||||
      immediate: true,
 | 
			
		||||
    },
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    showFloatBox,
 | 
			
		||||
    closeFloatBox,
 | 
			
		||||
    dragstartHandler,
 | 
			
		||||
    dragendHandler,
 | 
			
		||||
    floatBoxStates,
 | 
			
		||||
    floatBox,
 | 
			
		||||
    showingBoxKeys,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
@ -82,8 +82,10 @@ const showSrc = computed(() => uiService?.get('showSrc'));
 | 
			
		||||
const LEFT_COLUMN_WIDTH_STORAGE_KEY = '$MagicEditorLeftColumnWidthData';
 | 
			
		||||
const RIGHT_COLUMN_WIDTH_STORAGE_KEY = '$MagicEditorRightColumnWidthData';
 | 
			
		||||
 | 
			
		||||
const leftColumnWidthCacheData =
 | 
			
		||||
const getLeftColumnWidthCacheData = () =>
 | 
			
		||||
  Number(globalThis.localStorage.getItem(LEFT_COLUMN_WIDTH_STORAGE_KEY)) || DEFAULT_LEFT_COLUMN_WIDTH;
 | 
			
		||||
 | 
			
		||||
const leftColumnWidthCacheData = getLeftColumnWidthCacheData();
 | 
			
		||||
const RightColumnWidthCacheData =
 | 
			
		||||
  Number(globalThis.localStorage.getItem(RIGHT_COLUMN_WIDTH_STORAGE_KEY)) || DEFAULT_RIGHT_COLUMN_WIDTH;
 | 
			
		||||
 | 
			
		||||
@ -118,6 +120,13 @@ watch(
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => uiService?.get('hideSlideBar'),
 | 
			
		||||
  (hideSlideBar) => {
 | 
			
		||||
    columnWidth.value.left = hideSlideBar ? 0 : getLeftColumnWidthCacheData();
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const columnWidthChange = (columnW: GetColumnWidth) => {
 | 
			
		||||
  columnWidth.value.left = columnW.left;
 | 
			
		||||
  columnWidth.value.center = columnW.center;
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="m-editor-sidebar" v-if="data.type === 'tabs' && data.items.length">
 | 
			
		||||
  <div class="m-editor-sidebar" v-if="data.type === 'tabs' && data.items.length" v-show="!isHideSlideBar">
 | 
			
		||||
    <div class="m-editor-sidebar-header">
 | 
			
		||||
      <div
 | 
			
		||||
        class="m-editor-sidebar-header-item"
 | 
			
		||||
@ -7,6 +7,10 @@
 | 
			
		||||
        :key="config.$key ?? index"
 | 
			
		||||
        :class="{ 'is-active': activeTabName === config.text }"
 | 
			
		||||
        @click="activeTabName = config.text || `${index}`"
 | 
			
		||||
        draggable="true"
 | 
			
		||||
        @dragstart="dragstartHandler"
 | 
			
		||||
        @dragend="dragendHandler(config.$key, $event)"
 | 
			
		||||
        v-show="!floatBoxStates?.get(config.$key)?.status"
 | 
			
		||||
      >
 | 
			
		||||
        <MIcon v-if="config.icon" :icon="config.icon"></MIcon>
 | 
			
		||||
        <div v-if="config.text" class="magic-editor-tab-panel-title">{{ config.text }}</div>
 | 
			
		||||
@ -18,7 +22,12 @@
 | 
			
		||||
      :key="config.$key ?? index"
 | 
			
		||||
      v-show="activeTabName === config.text"
 | 
			
		||||
    >
 | 
			
		||||
      <component v-if="config" :is="config.component" v-bind="config.props || {}" v-on="config?.listeners || {}">
 | 
			
		||||
      <component
 | 
			
		||||
        v-if="config && !floatBoxStates?.get(config.$key)?.status"
 | 
			
		||||
        :is="config.component"
 | 
			
		||||
        v-bind="config.props || {}"
 | 
			
		||||
        v-on="config?.listeners || {}"
 | 
			
		||||
      >
 | 
			
		||||
        <template
 | 
			
		||||
          #component-list-panel-header
 | 
			
		||||
          v-if="config.$key === 'component-list' || config.slots?.componentListPanelHeader"
 | 
			
		||||
@ -80,15 +89,56 @@
 | 
			
		||||
      </component>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <Teleport to="body">
 | 
			
		||||
    <div class="m-editor-float-box-list">
 | 
			
		||||
      <div
 | 
			
		||||
        v-for="(config, index) in sideBarItems"
 | 
			
		||||
        :key="config.$key ?? index"
 | 
			
		||||
        ref="floatBox"
 | 
			
		||||
        :class="['m-editor-float-box', `m-editor-float-box-${config.$key}`]"
 | 
			
		||||
        :style="{
 | 
			
		||||
          left: `${floatBoxStates?.get(config.$key)?.left}px`,
 | 
			
		||||
          top: `${floatBoxStates?.get(config.$key)?.top}px`,
 | 
			
		||||
          zIndex: floatBoxStates?.get(config.$key)?.zIndex,
 | 
			
		||||
        }"
 | 
			
		||||
        v-show="floatBoxStates?.get(config.$key)?.status"
 | 
			
		||||
      >
 | 
			
		||||
        <div
 | 
			
		||||
          :class="['m-editor-float-box-header', `m-editor-float-box-header-${config.$key}`]"
 | 
			
		||||
          @click="showFloatBox(config.$key)"
 | 
			
		||||
        >
 | 
			
		||||
          <div>{{ config.text }}</div>
 | 
			
		||||
          <MIcon class="m-editor-float-box-close" :icon="Close" @click.stop="closeFloatBox(config.$key)"></MIcon>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="m-editor-float-box-body">
 | 
			
		||||
          <component
 | 
			
		||||
            v-if="config && floatBoxStates?.get(config.$key)?.status"
 | 
			
		||||
            :is="config.component"
 | 
			
		||||
            v-bind="{ ...config.props, slideType: 'box' }"
 | 
			
		||||
            v-on="config?.listeners || {}"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </Teleport>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, ref, watch } from 'vue';
 | 
			
		||||
import { Coin, EditPen, Goods, List } from '@element-plus/icons-vue';
 | 
			
		||||
import { computed, inject, ref, watch } from 'vue';
 | 
			
		||||
import { Close, Coin, EditPen, Goods, List } from '@element-plus/icons-vue';
 | 
			
		||||
 | 
			
		||||
import MIcon from '@editor/components/Icon.vue';
 | 
			
		||||
import type { MenuButton, MenuComponent, SidebarSlots, SideComponent, SideItem } from '@editor/type';
 | 
			
		||||
import { SideBarData } from '@editor/type';
 | 
			
		||||
import { useFloatBox } from '@editor/hooks/use-float-box';
 | 
			
		||||
import type {
 | 
			
		||||
  MenuButton,
 | 
			
		||||
  MenuComponent,
 | 
			
		||||
  Services,
 | 
			
		||||
  SideBarData,
 | 
			
		||||
  SidebarSlots,
 | 
			
		||||
  SideComponent,
 | 
			
		||||
  SideItem,
 | 
			
		||||
} from '@editor/type';
 | 
			
		||||
 | 
			
		||||
import CodeBlockListPanel from './code-block/CodeBlockListPanel.vue';
 | 
			
		||||
import DataSourceListPanel from './data-source/DataSourceListPanel.vue';
 | 
			
		||||
@ -111,6 +161,8 @@ const props = withDefaults(
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const services = inject<Services>('services');
 | 
			
		||||
 | 
			
		||||
const activeTabName = ref(props.data?.status);
 | 
			
		||||
 | 
			
		||||
const getItemConfig = (data: SideItem): SideComponent => {
 | 
			
		||||
@ -164,6 +216,29 @@ watch(
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const slideKeys = computed(() => sideBarItems.value.map((sideBarItem) => sideBarItem.$key));
 | 
			
		||||
const isHideSlideBar = computed(() => services?.uiService.get('hideSlideBar'));
 | 
			
		||||
 | 
			
		||||
const { showFloatBox, closeFloatBox, dragstartHandler, dragendHandler, floatBoxStates, floatBox, showingBoxKeys } =
 | 
			
		||||
  useFloatBox(slideKeys);
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
  () => showingBoxKeys.value,
 | 
			
		||||
  () => {
 | 
			
		||||
    const isActiveTabShow = showingBoxKeys.value.some(
 | 
			
		||||
      (key) => activeTabName.value === sideBarItems.value.find((v) => v.$key === key)?.text,
 | 
			
		||||
    );
 | 
			
		||||
    if (!isActiveTabShow) return;
 | 
			
		||||
    const nextSlideBarItem = sideBarItems.value.find((sideBarItem) => !showingBoxKeys.value.includes(sideBarItem.$key));
 | 
			
		||||
    if (!nextSlideBarItem) {
 | 
			
		||||
      services?.uiService.set('hideSlideBar', true);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    services?.uiService.set('hideSlideBar', false);
 | 
			
		||||
    activeTabName.value = nextSlideBarItem?.text;
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
  activeTabName,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -16,16 +16,16 @@
 | 
			
		||||
        <slot name="code-block-panel-tool" :id="id" :data="data"></slot>
 | 
			
		||||
      </template>
 | 
			
		||||
    </CodeBlockList>
 | 
			
		||||
 | 
			
		||||
    <!-- 代码块编辑区 -->
 | 
			
		||||
    <CodeBlockEditor
 | 
			
		||||
      v-if="codeConfig"
 | 
			
		||||
      ref="codeBlockEditor"
 | 
			
		||||
      :disabled="!editable"
 | 
			
		||||
      :content="codeConfig"
 | 
			
		||||
      @submit="submitCodeBlockHandler"
 | 
			
		||||
    ></CodeBlockEditor>
 | 
			
		||||
  </TMagicScrollbar>
 | 
			
		||||
  <!-- 代码块编辑区 -->
 | 
			
		||||
  <CodeBlockEditor
 | 
			
		||||
    v-if="codeConfig"
 | 
			
		||||
    ref="codeBlockEditor"
 | 
			
		||||
    :disabled="!editable"
 | 
			
		||||
    :content="codeConfig"
 | 
			
		||||
    :slideType="slideType"
 | 
			
		||||
    @submit="submitCodeBlockHandler"
 | 
			
		||||
  ></CodeBlockEditor>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
@ -37,7 +37,7 @@ import type { Id } from '@tmagic/schema';
 | 
			
		||||
import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
 | 
			
		||||
import SearchInput from '@editor/components/SearchInput.vue';
 | 
			
		||||
import { useCodeBlockEdit } from '@editor/hooks/use-code-block-edit';
 | 
			
		||||
import type { CodeBlockListPanelSlots, CodeDeleteErrorType, Services } from '@editor/type';
 | 
			
		||||
import type { CodeBlockListPanelSlots, CodeDeleteErrorType, Services, SlideType } from '@editor/type';
 | 
			
		||||
 | 
			
		||||
import CodeBlockList from './CodeBlockList.vue';
 | 
			
		||||
 | 
			
		||||
@ -49,6 +49,7 @@ defineOptions({
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
  customError?: (id: Id, errorType: CodeDeleteErrorType) => any;
 | 
			
		||||
  slideType?: SlideType;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const { codeBlockService } = inject<Services>('services') || {};
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <MFormDrawer
 | 
			
		||||
  <component
 | 
			
		||||
    :is="slideType === 'box' ? MFormBox : MFormDrawer"
 | 
			
		||||
    ref="fomDrawer"
 | 
			
		||||
    label-width="80px"
 | 
			
		||||
    :close-on-press-escape="false"
 | 
			
		||||
@ -10,17 +11,17 @@
 | 
			
		||||
    :disabled="disabled"
 | 
			
		||||
    @submit="submitHandler"
 | 
			
		||||
    @error="errorHandler"
 | 
			
		||||
  ></MFormDrawer>
 | 
			
		||||
  ></component>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, inject, ref, watchEffect } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { tMagicMessage } from '@tmagic/design';
 | 
			
		||||
import { FormConfig, MFormDrawer } from '@tmagic/form';
 | 
			
		||||
import { FormConfig, MFormBox, MFormDrawer } from '@tmagic/form';
 | 
			
		||||
import { DataSourceSchema } from '@tmagic/schema';
 | 
			
		||||
 | 
			
		||||
import type { Services } from '@editor/type';
 | 
			
		||||
import type { Services, SlideType } from '@editor/type';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'MEditorDataSourceConfigPanel',
 | 
			
		||||
@ -30,6 +31,7 @@ const props = defineProps<{
 | 
			
		||||
  title?: string;
 | 
			
		||||
  values: any;
 | 
			
		||||
  disabled: boolean;
 | 
			
		||||
  slideType?: SlideType;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['submit']);
 | 
			
		||||
 | 
			
		||||
@ -23,20 +23,16 @@
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 数据源列表 -->
 | 
			
		||||
    <DataSourceList @edit="editHandler" @remove="removeHandler">
 | 
			
		||||
      <template #data-source-panel-tool="{ data }">
 | 
			
		||||
        <slot name="data-source-panel-tool" :data="data"></slot>
 | 
			
		||||
      </template>
 | 
			
		||||
    </DataSourceList>
 | 
			
		||||
 | 
			
		||||
    <DataSourceConfigPanel
 | 
			
		||||
      ref="editDialog"
 | 
			
		||||
      :disabled="!editable"
 | 
			
		||||
      :values="dataSourceValues"
 | 
			
		||||
      :title="dialogTitle"
 | 
			
		||||
      @submit="submitDataSourceHandler"
 | 
			
		||||
    ></DataSourceConfigPanel>
 | 
			
		||||
    <DataSourceList @edit="editHandler" @remove="removeHandler"></DataSourceList>
 | 
			
		||||
  </TMagicScrollbar>
 | 
			
		||||
  <DataSourceConfigPanel
 | 
			
		||||
    ref="editDialog"
 | 
			
		||||
    :disabled="!editable"
 | 
			
		||||
    :values="dataSourceValues"
 | 
			
		||||
    :title="dialogTitle"
 | 
			
		||||
    :slideType="slideType"
 | 
			
		||||
    @submit="submitDataSourceHandler"
 | 
			
		||||
  ></DataSourceConfigPanel>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
@ -48,7 +44,7 @@ import type { DataSourceSchema } from '@tmagic/schema';
 | 
			
		||||
 | 
			
		||||
import SearchInput from '@editor/components/SearchInput.vue';
 | 
			
		||||
import ToolButton from '@editor/components/ToolButton.vue';
 | 
			
		||||
import type { DataSourceListSlots, Services } from '@editor/type';
 | 
			
		||||
import type { DataSourceListSlots, Services, SlideType } from '@editor/type';
 | 
			
		||||
 | 
			
		||||
import DataSourceConfigPanel from './DataSourceConfigPanel.vue';
 | 
			
		||||
import DataSourceList from './DataSourceList.vue';
 | 
			
		||||
@ -59,6 +55,10 @@ defineOptions({
 | 
			
		||||
  name: 'MEditorDataSourceListPanel',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
  slideType?: SlideType;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const { dataSourceService } = inject<Services>('services') || {};
 | 
			
		||||
 | 
			
		||||
const editDialog = ref<InstanceType<typeof DataSourceConfigPanel>>();
 | 
			
		||||
@ -102,7 +102,7 @@ const editHandler = (id: string) => {
 | 
			
		||||
    ...dataSourceService?.getDataSourceById(id),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  dialogTitle.value = `新增${dataSourceValues.value.title || ''}`;
 | 
			
		||||
  dialogTitle.value = `编辑${dataSourceValues.value.title || ''}`;
 | 
			
		||||
 | 
			
		||||
  editDialog.value.show();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -44,6 +44,8 @@ const state = reactive<UiState>({
 | 
			
		||||
  showRule: true,
 | 
			
		||||
  propsPanelSize: 'small',
 | 
			
		||||
  showAddPageButton: true,
 | 
			
		||||
  floatBox: new Map(),
 | 
			
		||||
  hideSlideBar: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
class Ui extends BaseService {
 | 
			
		||||
@ -95,6 +97,19 @@ class Ui extends BaseService {
 | 
			
		||||
    return Math.min((width - 60) / stageWidth || 1, (height - 80) / stageHeight || 1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async setFloatBox(keys: string[]) {
 | 
			
		||||
    const map = state.floatBox;
 | 
			
		||||
    for (const key of keys) {
 | 
			
		||||
      if (map.get(key)) continue;
 | 
			
		||||
      map.set(key, {
 | 
			
		||||
        status: false,
 | 
			
		||||
        zIndex: 99,
 | 
			
		||||
        top: 0,
 | 
			
		||||
        left: 0,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public resetState() {
 | 
			
		||||
    this.set('showSrc', false);
 | 
			
		||||
    this.set('uiSelectMode', false);
 | 
			
		||||
 | 
			
		||||
@ -33,4 +33,9 @@
 | 
			
		||||
  .el-drawer__body {
 | 
			
		||||
    padding: 10px 20px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.m-form-box {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    min-width: 872px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										48
									
								
								packages/editor/src/theme/floatbox.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								packages/editor/src/theme/floatbox.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,48 @@
 | 
			
		||||
.m-editor-float-box-list {
 | 
			
		||||
  .m-editor-float-box {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    width: auto;
 | 
			
		||||
    height: 966px;
 | 
			
		||||
    top: 240px;
 | 
			
		||||
    left: 240px;
 | 
			
		||||
    background: #fff;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
    z-index: 999;
 | 
			
		||||
    max-width: auto;
 | 
			
		||||
    min-width: auto;
 | 
			
		||||
    max-height: auto;
 | 
			
		||||
    min-height: auto;
 | 
			
		||||
    border: 1px solid #eee;
 | 
			
		||||
    box-shadow: 0 0 72px #ccc;
 | 
			
		||||
    &-header {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      padding: 0 16px;
 | 
			
		||||
      height: 44px;
 | 
			
		||||
      border-bottom: 1px solid #d8dee8;
 | 
			
		||||
      &:hover {
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    &-body {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex: 1;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      overflow: scroll;
 | 
			
		||||
      > *:first-child {
 | 
			
		||||
        min-width: 247px;
 | 
			
		||||
        border-right: 1px solid #d8dee8;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &-close {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      right: 16px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .moveable-resizable {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -40,6 +40,7 @@
 | 
			
		||||
    .magic-editor-tab-panel-title {
 | 
			
		||||
      font-size: 12px;
 | 
			
		||||
      white-space: normal;
 | 
			
		||||
      user-select: none;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -22,4 +22,5 @@
 | 
			
		||||
@import "./data-source-methods.scss";
 | 
			
		||||
@import "./data-source-input.scss";
 | 
			
		||||
@import "./key-value.scss";
 | 
			
		||||
@import "./floatbox.scss";
 | 
			
		||||
@import "./tree.scss";
 | 
			
		||||
 | 
			
		||||
@ -198,6 +198,20 @@ export interface UiState {
 | 
			
		||||
  propsPanelSize: 'large' | 'default' | 'small';
 | 
			
		||||
  /** 是否显示新增页面按钮 */
 | 
			
		||||
  showAddPageButton: boolean;
 | 
			
		||||
 | 
			
		||||
  /** slide 拖拽悬浮窗 state */
 | 
			
		||||
  floatBox: Map<
 | 
			
		||||
    string,
 | 
			
		||||
    {
 | 
			
		||||
      status: boolean;
 | 
			
		||||
      zIndex: number;
 | 
			
		||||
      top: number;
 | 
			
		||||
      left: number;
 | 
			
		||||
    }
 | 
			
		||||
  >;
 | 
			
		||||
 | 
			
		||||
  /** 是否隐藏侧边栏 */
 | 
			
		||||
  hideSlideBar: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EditorNodeInfo {
 | 
			
		||||
@ -318,6 +332,8 @@ export interface SideComponent extends MenuComponent {
 | 
			
		||||
  text: string;
 | 
			
		||||
  /** vue组件或url */
 | 
			
		||||
  icon: Component<{}, {}, any>;
 | 
			
		||||
  /** slide 唯一标识 key */
 | 
			
		||||
  $key: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -337,6 +353,12 @@ export interface SideBarData {
 | 
			
		||||
  items: SideItem[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * drawer 抽屉
 | 
			
		||||
 * box 悬浮窗
 | 
			
		||||
 */
 | 
			
		||||
export type SlideType = 'drawer' | 'box';
 | 
			
		||||
 | 
			
		||||
export interface ComponentItem {
 | 
			
		||||
  /** 显示文案 */
 | 
			
		||||
  text: string;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										106
									
								
								packages/form/src/FormBox.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								packages/form/src/FormBox.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,106 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="m-form-box">
 | 
			
		||||
    <div ref="boxBody" class="m-box-body">
 | 
			
		||||
      <Form
 | 
			
		||||
        ref="form"
 | 
			
		||||
        :size="size"
 | 
			
		||||
        :disabled="disabled"
 | 
			
		||||
        :config="config"
 | 
			
		||||
        :init-values="values"
 | 
			
		||||
        :parent-values="parentValues"
 | 
			
		||||
        :label-width="labelWidth"
 | 
			
		||||
        :label-position="labelPosition"
 | 
			
		||||
        :inline="inline"
 | 
			
		||||
        @change="changeHandler"
 | 
			
		||||
      ></Form>
 | 
			
		||||
      <slot></slot>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <TMagicRow class="dialog-footer">
 | 
			
		||||
      <TMagicCol :span="12" style="text-align: left">
 | 
			
		||||
        <div style="min-height: 1px">
 | 
			
		||||
          <slot name="left"></slot>
 | 
			
		||||
        </div>
 | 
			
		||||
      </TMagicCol>
 | 
			
		||||
      <TMagicCol :span="12">
 | 
			
		||||
        <slot name="footer">
 | 
			
		||||
          <TMagicButton type="primary" :disabled="disabled" :loading="saveFetch" @click="submitHandler">{{
 | 
			
		||||
            confirmText
 | 
			
		||||
          }}</TMagicButton>
 | 
			
		||||
        </slot>
 | 
			
		||||
      </TMagicCol>
 | 
			
		||||
    </TMagicRow>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, watchEffect } from 'vue';
 | 
			
		||||
 | 
			
		||||
import { TMagicButton, TMagicCol, TMagicRow } from '@tmagic/design';
 | 
			
		||||
 | 
			
		||||
import Form from './Form.vue';
 | 
			
		||||
import type { FormConfig } from './schema';
 | 
			
		||||
 | 
			
		||||
defineOptions({
 | 
			
		||||
  name: 'MFormDialog',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
withDefaults(
 | 
			
		||||
  defineProps<{
 | 
			
		||||
    config?: FormConfig;
 | 
			
		||||
    values?: Object;
 | 
			
		||||
    parentValues?: Object;
 | 
			
		||||
    width?: string | number;
 | 
			
		||||
    labelWidth?: string;
 | 
			
		||||
    disabled?: boolean;
 | 
			
		||||
    size?: 'small' | 'default' | 'large';
 | 
			
		||||
    confirmText?: string;
 | 
			
		||||
    inline?: boolean;
 | 
			
		||||
    labelPosition?: string;
 | 
			
		||||
  }>(),
 | 
			
		||||
  {
 | 
			
		||||
    config: () => [],
 | 
			
		||||
    values: () => ({}),
 | 
			
		||||
    confirmText: '确定',
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['submit', 'change', 'error']);
 | 
			
		||||
 | 
			
		||||
const form = ref<InstanceType<typeof Form>>();
 | 
			
		||||
const boxBody = ref<HTMLDivElement>();
 | 
			
		||||
const saveFetch = ref(false);
 | 
			
		||||
const bodyHeight = ref(0);
 | 
			
		||||
 | 
			
		||||
watchEffect(() => {
 | 
			
		||||
  if (boxBody.value) {
 | 
			
		||||
    bodyHeight.value = boxBody.value.clientHeight;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const submitHandler = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    const values = await form.value?.submitForm();
 | 
			
		||||
    emit('submit', values);
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    emit('error', e);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const changeHandler = (value: any) => {
 | 
			
		||||
  emit('change', value);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const show = () => {};
 | 
			
		||||
 | 
			
		||||
const hide = () => {};
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
  form,
 | 
			
		||||
  saveFetch,
 | 
			
		||||
  bodyHeight,
 | 
			
		||||
 | 
			
		||||
  show,
 | 
			
		||||
  hide,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@ -59,6 +59,7 @@ export * from './utils/useAddField';
 | 
			
		||||
export { default as MForm } from './Form.vue';
 | 
			
		||||
export { default as MFormDialog } from './FormDialog.vue';
 | 
			
		||||
export { default as MFormDrawer } from './FormDrawer.vue';
 | 
			
		||||
export { default as MFormBox } from './FormBox.vue';
 | 
			
		||||
export { default as MContainer } from './containers/Container.vue';
 | 
			
		||||
export { default as MFieldset } from './containers/Fieldset.vue';
 | 
			
		||||
export { default as MPanel } from './containers/Panel.vue';
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										18
									
								
								packages/form/src/theme/form-box.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								packages/form/src/theme/form-box.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
.m-form-box {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  padding: 16px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  .el-box__header {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .m-box-body {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
    overflow-x: hidden;
 | 
			
		||||
  }
 | 
			
		||||
  .dialog-footer {
 | 
			
		||||
    margin-top: 16px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -10,3 +10,4 @@
 | 
			
		||||
@use "./select.scss";
 | 
			
		||||
@use "./tabs.scss";
 | 
			
		||||
@use "./number-range.scss";
 | 
			
		||||
@use "./form-box.scss";
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user