roymondchen 8933c46b73 fix(editor): sidebar 侧栏项 key 与 SideItemKey 常量对齐
确保侧栏配置与字段编辑时切换 tab 的 key 一致,避免硬编码字符串不一致。
2026-06-16 14:12:14 +08:00

376 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="m-editor-sidebar" v-if="data.type === 'tabs' && data.items.length">
<div class="m-editor-sidebar-header">
<div
class="m-editor-sidebar-header-item"
v-for="(config, index) in sideBarItems"
v-show="!floatBoxStates[config.$key]?.status"
:draggable="config.draggable ?? true"
:key="config.$key ?? index"
:class="{ 'is-active': activeTabName === config.text }"
:style="config.tabStyle || {}"
@click="headerItemClickHandler(config, index)"
@dragstart="dragstartHandler"
@dragend="dragendHandler(config.$key, $event)"
>
<MIcon v-if="config.icon" :icon="config.icon"></MIcon>
<div v-if="config.text" class="magic-editor-tab-panel-title">{{ config.text }}</div>
</div>
</div>
<div
class="m-editor-sidebar-content"
:class="{ 'm-editor-dep-collecting': collecting }"
v-for="(config, index) in sideBarItems"
:key="config.$key ?? index"
v-show="[config.text, config.$key, `${index}`].includes(activeTabName)"
>
<component
v-if="config?.component && !floatBoxStates[config.$key]?.status"
:is="config.component"
v-bind="config.props || {}"
v-on="config?.listeners || {}"
>
<template
#component-list="{ componentGroupList }"
v-if="config.$key === 'component-list' || config.slots?.componentList"
>
<slot
v-if="config.$key === 'component-list'"
name="component-list"
:component-group-list="componentGroupList"
></slot>
<component v-else-if="config.slots?.componentList" :is="config.slots.componentList" />
</template>
<template
#component-list-panel-header
v-if="config.$key === 'component-list' || config.slots?.componentListPanelHeader"
>
<slot v-if="config.$key === 'component-list'" name="component-list-panel-header"></slot>
<component v-else-if="config.slots?.componentListPanelHeader" :is="config.slots.componentListPanelHeader" />
</template>
<template
#component-list-item="{ component }"
v-if="config.$key === 'component-list' || config.slots?.componentListItem"
>
<slot v-if="config.$key === 'component-list'" name="component-list-item" :component="component"></slot>
<component
v-else-if="config.slots?.componentListItem"
:is="config.slots.componentListItem"
:component="component"
/>
</template>
<template #layer-panel-header v-if="config.$key === 'layer' || config.slots?.layerPanelHeader">
<slot v-if="config.$key === 'layer'" name="layer-panel-header"></slot>
<component v-else-if="config.slots?.layerPanelHeader" :is="config.slots.layerPanelHeader" />
</template>
<template #code-block-panel-header v-if="config.$key === 'code-block' || config.slots?.codeBlockPanelHeader">
<slot v-if="config.$key === 'code-block'" name="code-block-panel-header"></slot>
<component v-else-if="config.slots?.codeBlockPanelHeader" :is="config.slots.codeBlockPanelHeader" />
</template>
<template
#code-block-panel-tool="{ id, data }"
v-if="config.$key === 'code-block' || config.slots?.codeBlockPanelTool"
>
<slot v-if="config.$key === 'code-block'" name="code-block-panel-tool" :id="id" :data="data"></slot>
<component v-else-if="config.slots?.codeBlockPanelTool" :is="config.slots.codeBlockPanelTool" />
</template>
<template #code-block-panel-search v-if="config.$key === 'code-block' || config.slots?.codeBlockPanelSearch">
<slot v-if="config.$key === 'code-block'" name="code-block-panel-search"></slot>
<component v-else-if="config.slots?.codeBlockPanelSearch" :is="config.slots.codeBlockPanelSearch" />
</template>
<template
#layer-node-content="{ data: nodeData }"
v-if="config.$key === 'layer' || config.slots?.layerNodeContent"
>
<slot v-if="config.$key === 'layer'" name="layer-node-content" :data="nodeData"></slot>
<component v-else-if="config.slots?.layerNodeContent" :is="config.slots.layerNodeContent" :data="nodeData" />
</template>
<template #layer-node-label="{ data: nodeData }" v-if="config.$key === 'layer' || config.slots?.layerNodeLabel">
<slot v-if="config.$key === 'layer'" name="layer-node-label" :data="nodeData"></slot>
<component v-else-if="config.slots?.layerNodeLabel" :is="config.slots.layerNodeTool" :data="nodeData" />
</template>
<template #layer-node-tool="{ data: nodeData }" v-if="config.$key === 'layer' || config.slots?.layerNodeTool">
<slot v-if="config.$key === 'layer'" name="layer-node-tool" :data="nodeData"></slot>
<component v-else-if="config.slots?.layerNodeTool" :is="config.slots.layerNodeTool" :data="nodeData" />
</template>
<template
#data-source-panel-tool="{ data }"
v-if="config.$key === 'data-source' || config.slots?.dataSourcePanelTool"
>
<slot v-if="config.$key === 'data-source'" name="data-source-panel-tool" :data="data"></slot>
<component v-else-if="config.slots?.DataSourcePanelTool" :is="config.slots.DataSourcePanelTool" />
</template>
<template #data-source-panel-search v-if="config.$key === 'data-source' || config.slots?.dataSourcePanelSearch">
<slot v-if="config.$key === 'data-source'" name="data-source-panel-search"></slot>
<component v-else-if="config.slots?.dataSourcePanelSearch" :is="config.slots.dataSourcePanelSearch" />
</template>
</component>
</div>
<div class="m-editor-sidebar-tips" v-if="tipsBarVisible && collecting && taskLength > 0">
<span>依赖收集中(剩余任务{{ taskLength }})</span>
<MIcon :icon="Close" class="close-icon" @click.stop="tipsBarVisible = false"></MIcon>
</div>
</div>
<Teleport to="body">
<template v-for="(config, index) in sideBarItems">
<FloatingBox
:key="config.$key ?? index"
v-if="floatBoxStates[config.$key]?.status"
v-model:visible="floatBoxStates[config.$key].status"
v-model:height="columnLeftHeight"
:width="columnLeftWidth"
:title="config.text"
:position="{
left: floatBoxStates[config.$key].left,
top: floatBoxStates[config.$key].top,
}"
>
<template #body>
<div class="m-editor-slide-list-box">
<component
v-if="config && floatBoxStates[config.$key].status"
:is="config.component"
v-bind="config.props || {}"
v-on="config?.listeners || {}"
/>
</div>
</template>
</FloatingBox>
</template>
</Teleport>
</template>
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { Close, Coin, EditPen, Goods, List } from '@element-plus/icons-vue';
import FloatingBox from '@editor/components/FloatingBox.vue';
import MIcon from '@editor/components/Icon.vue';
import { useEditorContentHeight } from '@editor/hooks/use-editor-content-height';
import { useFloatBox } from '@editor/hooks/use-float-box';
import { useServices } from '@editor/hooks/use-services';
import {
type CanDropInFunction,
ColumnLayout,
CustomContentMenuFunction,
type IsExpandableFunction,
type MenuButton,
type MenuComponent,
type SideBarData,
type SidebarSlots,
type SideComponent,
type SideItem,
SideItemKey,
type TreeNodeData,
} from '@editor/type';
import CodeBlockListPanel from './code-block/CodeBlockListPanel.vue';
import DataSourceListPanel from './data-source/DataSourceListPanel.vue';
import LayerPanel from './layer/LayerPanel.vue';
import ComponentListPanel from './ComponentListPanel.vue';
defineSlots<SidebarSlots>();
defineOptions({
name: 'MEditorSidebar',
});
const emit = defineEmits<{
'layer-node-dblclick': [event: MouseEvent, data: TreeNodeData];
}>();
const props = withDefaults(
defineProps<{
data?: SideBarData;
layerContentMenu: (MenuButton | MenuComponent)[];
indent?: number;
nextLevelIndentIncrement?: number;
customContentMenu: CustomContentMenuFunction;
/** 自定义判断组件树节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
layerNodeIsExpandable?: IsExpandableFunction;
/** 自定义判断当前正在拖动的源是否可以拖入目标节点内部,详见 EditorProps.canDropIn */
canDropIn?: CanDropInFunction;
/** 组件树节点双击前的钩子函数,返回 false 则阻止默认的双击行为 */
beforeLayerNodeDblclick?: (_event: MouseEvent, _data: TreeNodeData) => Promise<boolean | void> | boolean | void;
}>(),
{
data: () => ({
type: 'tabs',
status: '组件',
items: [SideItemKey.COMPONENT_LIST, SideItemKey.LAYER, SideItemKey.CODE_BLOCK, SideItemKey.DATA_SOURCE],
}),
},
);
const { depService, uiService, propsService } = useServices();
const collecting = computed(() => depService.get('collecting'));
const taskLength = computed(() => depService.get('taskLength'));
const tipsBarVisible = ref(true);
const columnLeftWidth = computed(() => uiService.get('columnWidth')[ColumnLayout.LEFT]);
const { height: editorContentHeight } = useEditorContentHeight();
const columnLeftHeight = ref(0);
const unWatchEditorContentHeight = watch(
editorContentHeight,
(height) => {
if (height) {
columnLeftHeight.value = height * 0.5;
nextTick().then(() => {
unWatchEditorContentHeight();
});
}
},
{
immediate: true,
},
);
const activeTabName = computed<string>({
get: () => uiService.get('sideBarActiveTabName'),
set: (value) => uiService.set('sideBarActiveTabName', value),
});
uiService.set('sideBarActiveTabName', props.data?.status || '');
const getItemConfig = (data: SideItem): SideComponent => {
const map: Record<string, SideComponent> = {
[SideItemKey.COMPONENT_LIST]: {
$key: SideItemKey.COMPONENT_LIST,
type: 'component',
icon: Goods,
text: '组件',
component: ComponentListPanel,
slots: {},
},
layer: {
$key: SideItemKey.LAYER,
type: 'component',
icon: List,
text: '已选组件',
props: {
layerContentMenu: props.layerContentMenu,
customContentMenu: props.customContentMenu,
indent: props.indent,
nextLevelIndentIncrement: props.nextLevelIndentIncrement,
isExpandable: props.layerNodeIsExpandable,
canDropIn: props.canDropIn,
beforeNodeDblclick: props.beforeLayerNodeDblclick,
},
listeners: {
'node-dblclick': (event: MouseEvent, nodeData: TreeNodeData) => emit('layer-node-dblclick', event, nodeData),
},
component: LayerPanel,
slots: {},
},
[SideItemKey.CODE_BLOCK]: {
$key: SideItemKey.CODE_BLOCK,
type: 'component',
icon: EditPen,
text: '代码编辑',
component: CodeBlockListPanel,
props: {
indent: props.indent,
nextLevelIndentIncrement: props.nextLevelIndentIncrement,
customContentMenu: props.customContentMenu,
},
slots: {},
},
[SideItemKey.DATA_SOURCE]: {
$key: SideItemKey.DATA_SOURCE,
type: 'component',
icon: Coin,
text: '数据源',
component: DataSourceListPanel,
props: {
indent: props.indent,
nextLevelIndentIncrement: props.nextLevelIndentIncrement,
customContentMenu: props.customContentMenu,
},
slots: {},
},
};
return typeof data === 'string' ? map[data] : data;
};
const sideBarItems = computed(() =>
props.data.items
.map((item) => getItemConfig(item))
.filter((item) => {
if (item.$key === SideItemKey.DATA_SOURCE) {
return !propsService.getDisabledDataSource();
}
if (item.$key === SideItemKey.CODE_BLOCK) {
return !propsService.getDisabledCodeBlock();
}
return true;
}),
);
watch(
sideBarItems,
(items) => {
uiService.set('sideBarItems', items);
},
{
immediate: true,
},
);
watch(
() => props.data.status,
(status) => {
activeTabName.value = status || '0';
},
);
const slideKeys = computed(() => sideBarItems.value.map((sideBarItem) => sideBarItem.$key));
const { dragstartHandler, dragendHandler, floatBoxStates, showingBoxKeys } = useFloatBox(slideKeys);
watch(
() => showingBoxKeys.value.length,
() => {
const isActiveTabShow = showingBoxKeys.value.some(
(key) => activeTabName.value === sideBarItems.value.find((v) => v.$key === key)?.text,
);
if (!isActiveTabShow && activeTabName.value) return;
const nextSlideBarItem = sideBarItems.value.find((sideBarItem) => !showingBoxKeys.value.includes(sideBarItem.$key));
if (!nextSlideBarItem) {
activeTabName.value = '';
uiService.set('hideSlideBar', true);
return;
}
uiService.set('hideSlideBar', false);
activeTabName.value = nextSlideBarItem?.text;
},
);
const headerItemClickHandler = async (config: SideComponent, index: number) => {
if (typeof config.beforeClick === 'function') {
if ((await config.beforeClick(config)) === false) {
return;
}
}
activeTabName.value = config.text || config.$key || `${index}`;
};
defineExpose({
activeTabName,
});
</script>