feat(editor): 查看/编辑时自动切换 sidebar 并定位到对应配置

点击代码块、数据源字段或方法的选择器编辑/查看按钮时,自动切换到对应 sidebar tab,并打开详情中的目标字段或方法配置。
This commit is contained in:
roymondchen 2026-06-15 17:41:36 +08:00
parent b35132e93e
commit ffcc734102
11 changed files with 190 additions and 45 deletions

View File

@ -107,10 +107,12 @@ const isCompareMode = computed(() => Boolean(props.isCompare && props.lastValues
const notEditable = computed(() => filterFunction(mForm, props.config.notEditable, props));
const hasCodeBlockSidePanel = computed(() =>
const codeBlockSidePanel = computed(() =>
(uiService.get('sideBarItems') || []).find((item) => item.$key === SideItemKey.CODE_BLOCK),
);
const hasCodeBlockSidePanel = computed(() => codeBlockSidePanel.value);
/**
* 根据代码块id获取代码块参数配置
* @param codeId 代码块ID
@ -191,6 +193,10 @@ const onParamsChangeHandler = (value: any, eventData: ContainerChangeEventData)
};
const editCode = (id: string) => {
const sideBarItem = codeBlockSidePanel.value;
if (sideBarItem) {
uiService.set('sideBarActiveTabName', sideBarItem.text || sideBarItem.$key || SideItemKey.CODE_BLOCK);
}
eventBus?.emit('edit-code', id);
};
</script>

View File

@ -211,11 +211,25 @@ const onChangeHandler = (v: string[] = []) => {
emit('change', v);
};
const hasDataSourceSidePanel = computed(() =>
const dataSourceSidePanel = computed(() =>
uiService.get('sideBarItems').find((item) => item.$key === SideItemKey.DATA_SOURCE),
);
const hasDataSourceSidePanel = computed(() => dataSourceSidePanel.value);
const editHandler = (id: string) => {
eventBus?.emit('edit-data-source', removeDataSourceFieldPrefix(id));
const sideBarItem = dataSourceSidePanel.value;
if (sideBarItem) {
uiService.set('sideBarActiveTabName', sideBarItem.text || sideBarItem.$key || SideItemKey.DATA_SOURCE);
}
const dataSourceId = removeDataSourceFieldPrefix(id);
const fieldPath = selectFieldsId.value;
if (fieldPath.length) {
eventBus?.emit('edit-data-source-field', dataSourceId, [...fieldPath]);
} else {
eventBus?.emit('edit-data-source', dataSourceId);
}
};
</script>

View File

@ -47,7 +47,7 @@
</template>
<script setup lang="ts">
import { computed, inject, Ref, ref } from 'vue';
import { computed, type ComputedRef, inject, onMounted, provide, Ref, ref } from 'vue';
import type { DataSchema } from '@tmagic/core';
import { TMagicButton, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
@ -101,6 +101,16 @@ const newHandler = () => {
addDialogVisible.value = true;
};
const editField = (row: Record<string, any>, index: number) => {
fieldValues.value = {
...row,
index,
};
fieldTitle.value = `编辑${row.title}`;
calcBoxPosition();
addDialogVisible.value = true;
};
const fieldChange = ({ index, ...value }: Record<string, any>, data: ContainerChangeEventData) => {
addDialogVisible.value = false;
@ -158,13 +168,7 @@ const fieldColumns: ColumnConfig[] = [
{
text: '编辑',
handler: (row: Record<string, any>, index: number) => {
fieldValues.value = {
...row,
index,
};
fieldTitle.value = `编辑${row.title}`;
calcBoxPosition();
addDialogVisible.value = true;
editField(row, index);
},
},
{
@ -354,4 +358,29 @@ const { height: editorHeight } = useEditorContentHeight();
const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null));
const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating);
/**
* DataSourceConfigPanel 注入打开数据源详情后需要直接打开的字段路径字段名数组
* 当前层消费 path[0]并把剩余路径下发给嵌套字段实现逐层打开
*/
const editingFieldPath = inject<ComputedRef<string[]>>(
'editingDataSourceFieldPath',
computed(() => []),
);
provide(
'editingDataSourceFieldPath',
computed(() => editingFieldPath.value.slice(1)),
);
onMounted(() => {
const path = editingFieldPath.value;
if (!path.length) return;
const fields: Record<string, any>[] = props.model[props.name] || [];
const index = fields.findIndex((field) => field.name === path[0]);
if (index === -1) return;
editField(fields[index], index);
});
</script>

View File

@ -13,7 +13,7 @@
></MCascader>
<TMagicTooltip
v-if="model[name] && isCustomMethod && hasDataSourceSidePanel && !isCompare"
v-if="model[name] && isCustomMethod && dataSourceSidePanel && !isCompare"
:content="notEditable ? '查看' : '编辑'"
>
<TMagicButton class="m-fields-select-action-button" :size="size" @click="editCodeHandler">
@ -75,7 +75,7 @@ const props = withDefaults(defineProps<FieldProps<DataSourceMethodSelectConfig>>
disabled: false,
});
const hasDataSourceSidePanel = computed(() =>
const dataSourceSidePanel = computed(() =>
(uiService.get('sideBarItems') || []).find((item) => item.$key === SideItemKey.DATA_SOURCE),
);
@ -208,12 +208,17 @@ const onParamsChangeHandler = (value: any, eventData: ContainerChangeEventData)
};
const editCodeHandler = () => {
const [id] = props.model[props.name];
const [id, methodName] = props.model[props.name];
const dataSource = dataSourceService.getDataSourceById(id);
if (!dataSource) return;
eventBus?.emit('edit-data-source', id);
const sideBarItem = dataSourceSidePanel.value;
if (sideBarItem) {
uiService.set('sideBarActiveTabName', sideBarItem.text || sideBarItem.$key || SideItemKey.DATA_SOURCE);
}
eventBus?.emit('edit-data-source-method', id, methodName);
};
</script>

View File

@ -21,7 +21,7 @@
</template>
<script setup lang="ts">
import { computed, inject, nextTick, ref, useTemplateRef } from 'vue';
import { computed, type ComputedRef, inject, nextTick, onMounted, ref, useTemplateRef } from 'vue';
import { cloneDeep } from 'lodash-es';
import type { CodeBlockContent } from '@tmagic/core';
@ -52,6 +52,29 @@ const codeBlockEditorRef = useTemplateRef<InstanceType<typeof CodeBlockEditor>>(
let editIndex = -1;
const editMethod = (method: CodeBlockContent, index: number) => {
let codeContent: string = '({ params, dataSource, app }) => {\n // place your code here\n}';
if (method.content) {
if (typeof method.content !== 'string') {
codeContent = method.content.toString();
} else {
codeContent = method.content;
}
}
codeConfig.value = {
...cloneDeep(method),
content: codeContent,
};
editIndex = index;
nextTick(() => {
codeBlockEditorRef.value?.show();
});
};
const methodColumns: ColumnConfig[] = [
{
label: '名称',
@ -77,26 +100,7 @@ const methodColumns: ColumnConfig[] = [
{
text: '编辑',
handler: (method: CodeBlockContent, index: number) => {
let codeContent: string = '({ params, dataSource, app }) => {\n // place your code here\n}';
if (method.content) {
if (typeof method.content !== 'string') {
codeContent = method.content.toString();
} else {
codeContent = method.content;
}
}
codeConfig.value = {
...cloneDeep(method),
content: codeContent,
};
editIndex = index;
nextTick(() => {
codeBlockEditorRef.value?.show();
});
editMethod(method, index);
},
},
{
@ -158,4 +162,21 @@ const submitCodeHandler = (value: CodeBlockContent, data: ContainerChangeEventDa
codeBlockEditorRef.value?.hide();
};
/** 由 DataSourceConfigPanel 注入:打开数据源详情后需要直接打开的方法名 */
const editingMethodName = inject<ComputedRef<string | undefined>>(
'editingDataSourceMethodName',
computed(() => ''),
);
onMounted(() => {
const methodName = editingMethodName.value;
if (!methodName) return;
const methods: CodeBlockContent[] = props.model[props.name] || [];
const index = methods.findIndex((method) => method.name === methodName);
if (index === -1) return;
editMethod(methods[index], index);
});
</script>

View File

@ -22,7 +22,7 @@
</template>
<script setup lang="ts">
import { inject, nextTick, Ref, ref, watch, watchEffect } from 'vue';
import { computed, inject, nextTick, provide, Ref, ref, watch, watchEffect } from 'vue';
import type { DataSourceSchema } from '@tmagic/core';
import { tMagicMessage } from '@tmagic/design';
@ -41,6 +41,10 @@ const props = defineProps<{
title?: string;
values: any;
disabled: boolean;
/** 打开后需要直接定位并打开的方法名传入时默认激活「方法定义」tab */
editMethodName?: string;
/** 打开后需要直接定位并打开的字段路径传入时默认激活「数据定义」tab */
editFieldPath?: string[];
}>();
const boxVisible = defineModel<boolean>('visible', { default: false });
@ -62,9 +66,33 @@ const { height: editorHeight } = useEditorContentHeight();
const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null));
const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating);
/** 供「方法定义」tab 内的字段消费,用于打开数据源详情后自动打开指定方法 */
provide(
'editingDataSourceMethodName',
computed(() => props.editMethodName),
);
/** 供「数据定义」tab 内的字段消费,用于打开数据源详情后自动打开指定字段 */
provide(
'editingDataSourceFieldPath',
computed(() => props.editFieldPath || []),
);
watchEffect(() => {
initValues.value = props.values;
dataSourceConfig.value = dataSourceService.getFormConfig(initValues.value.type);
const config = dataSourceService.getFormConfig(initValues.value.type);
// / tab tabstatus: methods / fields
let activeTab = '';
if (props.editMethodName) {
activeTab = 'methods';
} else if (props.editFieldPath?.length) {
activeTab = 'fields';
}
dataSourceConfig.value = activeTab
? config.map((item) => ((item as { type?: string }).type === 'tab' ? { ...item, active: activeTab } : item))
: config;
});
const submitHandler = (values: any, data: ContainerChangeEventData) => {

View File

@ -29,6 +29,8 @@
:disabled="!editable"
:values="dataSourceValues"
:title="dialogTitle"
:edit-method-name="editMethodName"
:edit-field-path="editFieldPath"
@submit="submitDataSourceHandler"
@close="editDialogCloseHandler"
></DataSourceConfigPanel>
@ -45,7 +47,7 @@
</template>
<script setup lang="ts">
import { computed, inject, useTemplateRef, watch } from 'vue';
import { computed, inject, ref, useTemplateRef, watch } from 'vue';
import { mergeWith } from 'lodash-es';
import { tMagicMessageBox, TMagicScrollbar } from '@tmagic/design';
@ -79,7 +81,16 @@ const { dataSourceService } = useServices();
const { editDialog, dataSourceValues, dialogTitle, editable, editHandler, submitDataSourceHandler } =
useDataSourceEdit(dataSourceService);
/** 打开数据源详情时需要直接定位并打开的方法名为空则正常展示「数据定义」tab */
const editMethodName = ref('');
/** 打开数据源详情时需要直接定位并打开的字段路径,为空则不自动打开字段配置 */
const editFieldPath = ref<string[]>([]);
const editDialogCloseHandler = () => {
editMethodName.value = '';
editFieldPath.value = [];
if (dataSourceListRef.value) {
for (const [, status] of dataSourceListRef.value.nodeStatusMap.entries()) {
status.selected = false;
@ -139,6 +150,20 @@ const filterTextChangeHandler = (val: string) => {
};
eventBus?.on('edit-data-source', (id: string) => {
editMethodName.value = '';
editFieldPath.value = [];
editHandler(id);
});
eventBus?.on('edit-data-source-method', (id: string, methodName: string) => {
editMethodName.value = methodName;
editFieldPath.value = [];
editHandler(id);
});
eventBus?.on('edit-data-source-field', (id: string, fieldPath: string[]) => {
editMethodName.value = '';
editFieldPath.value = fieldPath;
editHandler(id);
});

View File

@ -1156,6 +1156,8 @@ export type SyncHookPlugin<
export interface EventBusEvent {
'edit-data-source': [id: string];
'edit-data-source-method': [id: string, methodName: string];
'edit-data-source-field': [id: string, fieldPath: string[]];
'remove-data-source': [id: string];
'edit-code': [id: string];
}

View File

@ -10,6 +10,7 @@ const dataSourceFormConfig: TabConfig = {
items: [
{
title: '数据定义',
status: 'fields',
items: [
{
name: 'fields',
@ -20,6 +21,7 @@ const dataSourceFormConfig: TabConfig = {
},
{
title: '方法定义',
status: 'methods',
items: [
{
name: 'methods',

View File

@ -14,7 +14,7 @@ const { messageError } = vi.hoisted(() => ({ messageError: vi.fn() }));
const dataSourceService = { get: vi.fn() };
const propsService = { getDisabledDataSource: vi.fn() };
const uiService = { get: vi.fn() };
const uiService = { get: vi.fn(), set: vi.fn() };
vi.mock('@editor/hooks/use-services', () => ({
useServices: () => ({ dataSourceService, propsService, uiService }),
@ -158,13 +158,25 @@ describe('FieldSelect', () => {
expect(wrapper.emitted('change')?.[0]?.[0]).toEqual(['ds1', 'a']);
});
test('editHandler emit edit-data-source 到 eventBus', () => {
test('editHandler 无字段时 emit edit-data-source 并切换数据源 tab', async () => {
const eventBusEmit = vi.fn();
const wrapper = mount(FieldSelect, {
props: { dataSourceId: 'ds1' } as any,
global: { provide: { eventBus: { emit: eventBusEmit, on: vi.fn() } } },
});
expect(wrapper).toBeTruthy();
await wrapper.find('.m-fields-select-action-button').trigger('click');
expect(uiService.set).toHaveBeenCalledWith('sideBarActiveTabName', 'data-source');
expect(eventBusEmit).toHaveBeenCalledWith('edit-data-source', 'ds1');
});
test('editHandler 有字段时 emit edit-data-source-field 带字段路径', async () => {
const eventBusEmit = vi.fn();
const wrapper = mount(FieldSelect, {
props: { dataSourceId: 'ds1', modelValue: ['a'] } as any,
global: { provide: { eventBus: { emit: eventBusEmit, on: vi.fn() } } },
});
await wrapper.find('.m-fields-select-action-button').trigger('click');
expect(eventBusEmit).toHaveBeenCalledWith('edit-data-source-field', 'ds1', ['a']);
});
});

View File

@ -22,7 +22,7 @@ const dataSourceService = {
getFormMethod: vi.fn(() => []),
};
const uiService = { get: vi.fn(() => [{ $key: 'data-source' }]) };
const uiService = { get: vi.fn(() => [{ $key: 'data-source' }]), set: vi.fn() };
vi.mock('@editor/hooks/use-services', () => ({
useServices: () => ({ dataSourceService, uiService }),
@ -158,7 +158,7 @@ describe('DataSourceMethodSelect', () => {
expect(evts).toBeTruthy();
});
test('编辑按钮 emit edit-data-source', async () => {
test('编辑按钮 emit edit-data-source-method 并切换到数据源 tab', async () => {
dataSourceService.getDataSourceById.mockReturnValue({ id: 'ds1', methods: [{ name: 'doFetch' }] });
const eventBus = { emit: vi.fn() };
const wrapper = mount(DataSourceMethodSelect, {
@ -166,7 +166,8 @@ describe('DataSourceMethodSelect', () => {
global: { provide: { eventBus } },
});
await wrapper.find('button').trigger('click');
expect(eventBus.emit).toHaveBeenCalledWith('edit-data-source', 'ds1');
expect(uiService.set).toHaveBeenCalledWith('sideBarActiveTabName', 'data-source');
expect(eventBus.emit).toHaveBeenCalledWith('edit-data-source-method', 'ds1', 'doFetch');
});
test('编辑按钮: 找不到 dataSource 时不触发', async () => {