mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-04-06 03:57:56 +08:00
feat(editor): 支持 slide 侧边栏可拖拽悬浮
This commit is contained in:
parent
9098504e5f
commit
2b10e7eda9
@ -61,6 +61,7 @@
|
|||||||
"keycon": "^1.4.0",
|
"keycon": "^1.4.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"monaco-editor": "^0.41.0",
|
"monaco-editor": "^0.41.0",
|
||||||
|
"moveable": "^0.51.1",
|
||||||
"serialize-javascript": "^6.0.0",
|
"serialize-javascript": "^6.0.0",
|
||||||
"vue": "^3.3.4"
|
"vue": "^3.3.4"
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<MFormDrawer
|
<component
|
||||||
|
:is="slideType === 'box' ? MFormBox : MFormDrawer"
|
||||||
class="m-editor-code-block-editor"
|
class="m-editor-code-block-editor"
|
||||||
ref="fomDrawer"
|
ref="fomDrawer"
|
||||||
label-width="80px"
|
label-width="80px"
|
||||||
@ -19,7 +20,7 @@
|
|||||||
<template #left>
|
<template #left>
|
||||||
<TMagicButton type="primary" text @click="difVisible = true">查看修改</TMagicButton>
|
<TMagicButton type="primary" text @click="difVisible = true">查看修改</TMagicButton>
|
||||||
</template>
|
</template>
|
||||||
</MFormDrawer>
|
</component>
|
||||||
|
|
||||||
<TMagicDialog v-model="difVisible" title="查看修改" fullscreen>
|
<TMagicDialog v-model="difVisible" title="查看修改" fullscreen>
|
||||||
<div style="display: flex; margin-bottom: 10px">
|
<div style="display: flex; margin-bottom: 10px">
|
||||||
@ -50,11 +51,11 @@
|
|||||||
import { computed, inject, onUnmounted, ref } from 'vue';
|
import { computed, inject, onUnmounted, ref } from 'vue';
|
||||||
|
|
||||||
import { TMagicButton, TMagicDialog, tMagicMessage, tMagicMessageBox, TMagicTag } from '@tmagic/design';
|
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 type { CodeBlockContent } from '@tmagic/schema';
|
||||||
|
|
||||||
import CodeEditor from '@editor/layouts/CodeEditor.vue';
|
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';
|
import { getConfig } from '@editor/utils/config';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@ -66,6 +67,7 @@ const props = defineProps<{
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isDataSource?: boolean;
|
isDataSource?: boolean;
|
||||||
dataSourceType?: string;
|
dataSourceType?: string;
|
||||||
|
slideType?: SlideType;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -19,3 +19,4 @@
|
|||||||
export * from './use-code-block-edit';
|
export * from './use-code-block-edit';
|
||||||
export * from './use-data-source-method';
|
export * from './use-data-source-method';
|
||||||
export * from './use-stage';
|
export * from './use-stage';
|
||||||
|
export * from './use-float-box';
|
||||||
|
@ -68,6 +68,8 @@ export const useCodeBlockEdit = (codeBlockService?: CodeBlockService) => {
|
|||||||
|
|
||||||
await codeBlockService?.setCodeDslById(codeId.value, values);
|
await codeBlockService?.setCodeDslById(codeId.value, values);
|
||||||
|
|
||||||
|
tMagicMessage.success('代码块保存成功');
|
||||||
|
|
||||||
codeBlockEditor.value?.hide();
|
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 LEFT_COLUMN_WIDTH_STORAGE_KEY = '$MagicEditorLeftColumnWidthData';
|
||||||
const RIGHT_COLUMN_WIDTH_STORAGE_KEY = '$MagicEditorRightColumnWidthData';
|
const RIGHT_COLUMN_WIDTH_STORAGE_KEY = '$MagicEditorRightColumnWidthData';
|
||||||
|
|
||||||
const leftColumnWidthCacheData =
|
const getLeftColumnWidthCacheData = () =>
|
||||||
Number(globalThis.localStorage.getItem(LEFT_COLUMN_WIDTH_STORAGE_KEY)) || DEFAULT_LEFT_COLUMN_WIDTH;
|
Number(globalThis.localStorage.getItem(LEFT_COLUMN_WIDTH_STORAGE_KEY)) || DEFAULT_LEFT_COLUMN_WIDTH;
|
||||||
|
|
||||||
|
const leftColumnWidthCacheData = getLeftColumnWidthCacheData();
|
||||||
const RightColumnWidthCacheData =
|
const RightColumnWidthCacheData =
|
||||||
Number(globalThis.localStorage.getItem(RIGHT_COLUMN_WIDTH_STORAGE_KEY)) || DEFAULT_RIGHT_COLUMN_WIDTH;
|
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) => {
|
const columnWidthChange = (columnW: GetColumnWidth) => {
|
||||||
columnWidth.value.left = columnW.left;
|
columnWidth.value.left = columnW.left;
|
||||||
columnWidth.value.center = columnW.center;
|
columnWidth.value.center = columnW.center;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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">
|
||||||
<div
|
<div
|
||||||
class="m-editor-sidebar-header-item"
|
class="m-editor-sidebar-header-item"
|
||||||
@ -7,6 +7,10 @@
|
|||||||
:key="config.$key ?? index"
|
:key="config.$key ?? index"
|
||||||
:class="{ 'is-active': activeTabName === config.text }"
|
:class="{ 'is-active': activeTabName === config.text }"
|
||||||
@click="activeTabName = config.text || `${index}`"
|
@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>
|
<MIcon v-if="config.icon" :icon="config.icon"></MIcon>
|
||||||
<div v-if="config.text" class="magic-editor-tab-panel-title">{{ config.text }}</div>
|
<div v-if="config.text" class="magic-editor-tab-panel-title">{{ config.text }}</div>
|
||||||
@ -18,7 +22,12 @@
|
|||||||
:key="config.$key ?? index"
|
:key="config.$key ?? index"
|
||||||
v-show="activeTabName === config.text"
|
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
|
<template
|
||||||
#component-list-panel-header
|
#component-list-panel-header
|
||||||
v-if="config.$key === 'component-list' || config.slots?.componentListPanelHeader"
|
v-if="config.$key === 'component-list' || config.slots?.componentListPanelHeader"
|
||||||
@ -80,15 +89,56 @@
|
|||||||
</component>
|
</component>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, inject, ref, watch } from 'vue';
|
||||||
import { Coin, EditPen, Goods, List } from '@element-plus/icons-vue';
|
import { Close, Coin, EditPen, Goods, List } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
import MIcon from '@editor/components/Icon.vue';
|
import MIcon from '@editor/components/Icon.vue';
|
||||||
import type { MenuButton, MenuComponent, SidebarSlots, SideComponent, SideItem } from '@editor/type';
|
import { useFloatBox } from '@editor/hooks/use-float-box';
|
||||||
import { SideBarData } from '@editor/type';
|
import type {
|
||||||
|
MenuButton,
|
||||||
|
MenuComponent,
|
||||||
|
Services,
|
||||||
|
SideBarData,
|
||||||
|
SidebarSlots,
|
||||||
|
SideComponent,
|
||||||
|
SideItem,
|
||||||
|
} from '@editor/type';
|
||||||
|
|
||||||
import CodeBlockListPanel from './code-block/CodeBlockListPanel.vue';
|
import CodeBlockListPanel from './code-block/CodeBlockListPanel.vue';
|
||||||
import DataSourceListPanel from './data-source/DataSourceListPanel.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 activeTabName = ref(props.data?.status);
|
||||||
|
|
||||||
const getItemConfig = (data: SideItem): SideComponent => {
|
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({
|
defineExpose({
|
||||||
activeTabName,
|
activeTabName,
|
||||||
});
|
});
|
||||||
|
@ -16,16 +16,16 @@
|
|||||||
<slot name="code-block-panel-tool" :id="id" :data="data"></slot>
|
<slot name="code-block-panel-tool" :id="id" :data="data"></slot>
|
||||||
</template>
|
</template>
|
||||||
</CodeBlockList>
|
</CodeBlockList>
|
||||||
|
|
||||||
<!-- 代码块编辑区 -->
|
|
||||||
<CodeBlockEditor
|
|
||||||
v-if="codeConfig"
|
|
||||||
ref="codeBlockEditor"
|
|
||||||
:disabled="!editable"
|
|
||||||
:content="codeConfig"
|
|
||||||
@submit="submitCodeBlockHandler"
|
|
||||||
></CodeBlockEditor>
|
|
||||||
</TMagicScrollbar>
|
</TMagicScrollbar>
|
||||||
|
<!-- 代码块编辑区 -->
|
||||||
|
<CodeBlockEditor
|
||||||
|
v-if="codeConfig"
|
||||||
|
ref="codeBlockEditor"
|
||||||
|
:disabled="!editable"
|
||||||
|
:content="codeConfig"
|
||||||
|
:slideType="slideType"
|
||||||
|
@submit="submitCodeBlockHandler"
|
||||||
|
></CodeBlockEditor>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -37,7 +37,7 @@ import type { Id } from '@tmagic/schema';
|
|||||||
import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
|
import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
|
||||||
import SearchInput from '@editor/components/SearchInput.vue';
|
import SearchInput from '@editor/components/SearchInput.vue';
|
||||||
import { useCodeBlockEdit } from '@editor/hooks/use-code-block-edit';
|
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';
|
import CodeBlockList from './CodeBlockList.vue';
|
||||||
|
|
||||||
@ -49,6 +49,7 @@ defineOptions({
|
|||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
customError?: (id: Id, errorType: CodeDeleteErrorType) => any;
|
customError?: (id: Id, errorType: CodeDeleteErrorType) => any;
|
||||||
|
slideType?: SlideType;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { codeBlockService } = inject<Services>('services') || {};
|
const { codeBlockService } = inject<Services>('services') || {};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<MFormDrawer
|
<component
|
||||||
|
:is="slideType === 'box' ? MFormBox : MFormDrawer"
|
||||||
ref="fomDrawer"
|
ref="fomDrawer"
|
||||||
label-width="80px"
|
label-width="80px"
|
||||||
:close-on-press-escape="false"
|
:close-on-press-escape="false"
|
||||||
@ -10,17 +11,17 @@
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@submit="submitHandler"
|
@submit="submitHandler"
|
||||||
@error="errorHandler"
|
@error="errorHandler"
|
||||||
></MFormDrawer>
|
></component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, inject, ref, watchEffect } from 'vue';
|
import { computed, inject, ref, watchEffect } from 'vue';
|
||||||
|
|
||||||
import { tMagicMessage } from '@tmagic/design';
|
import { tMagicMessage } from '@tmagic/design';
|
||||||
import { FormConfig, MFormDrawer } from '@tmagic/form';
|
import { FormConfig, MFormBox, MFormDrawer } from '@tmagic/form';
|
||||||
import { DataSourceSchema } from '@tmagic/schema';
|
import { DataSourceSchema } from '@tmagic/schema';
|
||||||
|
|
||||||
import type { Services } from '@editor/type';
|
import type { Services, SlideType } from '@editor/type';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MEditorDataSourceConfigPanel',
|
name: 'MEditorDataSourceConfigPanel',
|
||||||
@ -30,6 +31,7 @@ const props = defineProps<{
|
|||||||
title?: string;
|
title?: string;
|
||||||
values: any;
|
values: any;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
slideType?: SlideType;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits(['submit']);
|
const emit = defineEmits(['submit']);
|
||||||
|
@ -23,20 +23,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 数据源列表 -->
|
<!-- 数据源列表 -->
|
||||||
<DataSourceList @edit="editHandler" @remove="removeHandler">
|
<DataSourceList @edit="editHandler" @remove="removeHandler"></DataSourceList>
|
||||||
<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>
|
|
||||||
</TMagicScrollbar>
|
</TMagicScrollbar>
|
||||||
|
<DataSourceConfigPanel
|
||||||
|
ref="editDialog"
|
||||||
|
:disabled="!editable"
|
||||||
|
:values="dataSourceValues"
|
||||||
|
:title="dialogTitle"
|
||||||
|
:slideType="slideType"
|
||||||
|
@submit="submitDataSourceHandler"
|
||||||
|
></DataSourceConfigPanel>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -48,7 +44,7 @@ import type { DataSourceSchema } from '@tmagic/schema';
|
|||||||
|
|
||||||
import SearchInput from '@editor/components/SearchInput.vue';
|
import SearchInput from '@editor/components/SearchInput.vue';
|
||||||
import ToolButton from '@editor/components/ToolButton.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 DataSourceConfigPanel from './DataSourceConfigPanel.vue';
|
||||||
import DataSourceList from './DataSourceList.vue';
|
import DataSourceList from './DataSourceList.vue';
|
||||||
@ -59,6 +55,10 @@ defineOptions({
|
|||||||
name: 'MEditorDataSourceListPanel',
|
name: 'MEditorDataSourceListPanel',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
slideType?: SlideType;
|
||||||
|
}>();
|
||||||
|
|
||||||
const { dataSourceService } = inject<Services>('services') || {};
|
const { dataSourceService } = inject<Services>('services') || {};
|
||||||
|
|
||||||
const editDialog = ref<InstanceType<typeof DataSourceConfigPanel>>();
|
const editDialog = ref<InstanceType<typeof DataSourceConfigPanel>>();
|
||||||
@ -102,7 +102,7 @@ const editHandler = (id: string) => {
|
|||||||
...dataSourceService?.getDataSourceById(id),
|
...dataSourceService?.getDataSourceById(id),
|
||||||
};
|
};
|
||||||
|
|
||||||
dialogTitle.value = `新增${dataSourceValues.value.title || ''}`;
|
dialogTitle.value = `编辑${dataSourceValues.value.title || ''}`;
|
||||||
|
|
||||||
editDialog.value.show();
|
editDialog.value.show();
|
||||||
};
|
};
|
||||||
|
@ -44,6 +44,8 @@ const state = reactive<UiState>({
|
|||||||
showRule: true,
|
showRule: true,
|
||||||
propsPanelSize: 'small',
|
propsPanelSize: 'small',
|
||||||
showAddPageButton: true,
|
showAddPageButton: true,
|
||||||
|
floatBox: new Map(),
|
||||||
|
hideSlideBar: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
class Ui extends BaseService {
|
class Ui extends BaseService {
|
||||||
@ -95,6 +97,19 @@ class Ui extends BaseService {
|
|||||||
return Math.min((width - 60) / stageWidth || 1, (height - 80) / stageHeight || 1);
|
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() {
|
public resetState() {
|
||||||
this.set('showSrc', false);
|
this.set('showSrc', false);
|
||||||
this.set('uiSelectMode', false);
|
this.set('uiSelectMode', false);
|
||||||
|
@ -33,4 +33,9 @@
|
|||||||
.el-drawer__body {
|
.el-drawer__body {
|
||||||
padding: 10px 20px;
|
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 {
|
.magic-editor-tab-panel-title {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,4 +22,5 @@
|
|||||||
@import "./data-source-methods.scss";
|
@import "./data-source-methods.scss";
|
||||||
@import "./data-source-input.scss";
|
@import "./data-source-input.scss";
|
||||||
@import "./key-value.scss";
|
@import "./key-value.scss";
|
||||||
|
@import "./floatbox.scss";
|
||||||
@import "./tree.scss";
|
@import "./tree.scss";
|
||||||
|
@ -198,6 +198,20 @@ export interface UiState {
|
|||||||
propsPanelSize: 'large' | 'default' | 'small';
|
propsPanelSize: 'large' | 'default' | 'small';
|
||||||
/** 是否显示新增页面按钮 */
|
/** 是否显示新增页面按钮 */
|
||||||
showAddPageButton: boolean;
|
showAddPageButton: boolean;
|
||||||
|
|
||||||
|
/** slide 拖拽悬浮窗 state */
|
||||||
|
floatBox: Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
status: boolean;
|
||||||
|
zIndex: number;
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
/** 是否隐藏侧边栏 */
|
||||||
|
hideSlideBar: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorNodeInfo {
|
export interface EditorNodeInfo {
|
||||||
@ -318,6 +332,8 @@ export interface SideComponent extends MenuComponent {
|
|||||||
text: string;
|
text: string;
|
||||||
/** vue组件或url */
|
/** vue组件或url */
|
||||||
icon: Component<{}, {}, any>;
|
icon: Component<{}, {}, any>;
|
||||||
|
/** slide 唯一标识 key */
|
||||||
|
$key: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -337,6 +353,12 @@ export interface SideBarData {
|
|||||||
items: SideItem[];
|
items: SideItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* drawer 抽屉
|
||||||
|
* box 悬浮窗
|
||||||
|
*/
|
||||||
|
export type SlideType = 'drawer' | 'box';
|
||||||
|
|
||||||
export interface ComponentItem {
|
export interface ComponentItem {
|
||||||
/** 显示文案 */
|
/** 显示文案 */
|
||||||
text: string;
|
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 MForm } from './Form.vue';
|
||||||
export { default as MFormDialog } from './FormDialog.vue';
|
export { default as MFormDialog } from './FormDialog.vue';
|
||||||
export { default as MFormDrawer } from './FormDrawer.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 MContainer } from './containers/Container.vue';
|
||||||
export { default as MFieldset } from './containers/Fieldset.vue';
|
export { default as MFieldset } from './containers/Fieldset.vue';
|
||||||
export { default as MPanel } from './containers/Panel.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 "./select.scss";
|
||||||
@use "./tabs.scss";
|
@use "./tabs.scss";
|
||||||
@use "./number-range.scss";
|
@use "./number-range.scss";
|
||||||
|
@use "./form-box.scss";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user