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",
|
||||
"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