refactor(editor): 解耦 FloatingBox 的 uiService 依赖并改为 props 传入

- FloatingBox 不再强制依赖 uiService
- frameworkWidth 默认回退到视窗宽度
- 新增 initialStyle prop 支持外部设置初始样式
- 各调用方显式传入 frameworkWidth 以保留右边界收敛行为
This commit is contained in:
roymondchen 2026-07-02 20:22:24 +08:00
parent 284be0d276
commit 3b9fb714e5
9 changed files with 91 additions and 9 deletions

View File

@ -6,6 +6,7 @@
v-model:height="codeBlockEditorHeight"
:body-style="{ padding: '0 16px' }"
:title="content.name ? `${disabled ? '查看' : '编辑'}${content.name}` : '新增代码'"
:framework-width="frameworkWidth"
:position="boxPosition"
:before-close="beforeClose"
>
@ -207,6 +208,7 @@ const closedHandler = () => {
const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null));
const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating);
const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
watch(boxVisible, (visible) => {
nextTick(() => {

View File

@ -30,7 +30,6 @@ import VanillaMoveable from 'moveable';
import { TMagicButton, useZIndex } from '@tmagic/design';
import MIcon from '@editor/components/Icon.vue';
import { useServices } from '@editor/hooks/use-services';
interface Position {
left: number;
@ -46,11 +45,17 @@ const props = withDefaults(
position?: Position;
title?: string;
bodyStyle?: CSSProperties;
/** 浮窗初始样式,会与内部计算样式合并,外部传入优先 */
initialStyle?: CSSProperties;
/** 用于约束浮窗 left 的容器宽度,传入时按宽度收敛 left避免超出右边界默认取视窗宽度 */
frameworkWidth?: number;
beforeClose?: (_done: (_cancel?: boolean) => void) => void;
}>(),
{
title: '',
position: () => ({ left: 0, top: 0 }),
initialStyle: () => ({}),
frameworkWidth: 0,
},
);
@ -73,12 +78,11 @@ const bodyHeight = computed(() => {
return 'auto';
});
const { uiService } = useServices();
const frameworkWidth = computed(() => uiService.get('frameworkRect').width || 0);
const style = computed(() => {
let { left } = props.position;
if (width.value) {
left = left + width.value > frameworkWidth.value ? frameworkWidth.value - width.value : left;
const frameworkWidth = props.frameworkWidth || globalThis.window?.innerWidth || 0;
if (width.value && frameworkWidth) {
left = left + width.value > frameworkWidth ? frameworkWidth - width.value : left;
}
return {
@ -86,6 +90,7 @@ const style = computed(() => {
top: `${props.position.top}px`,
width: width.value ? `${width.value}px` : 'auto',
height: height.value ? `${height.value}px` : 'auto',
...props.initialStyle,
};
});

View File

@ -13,6 +13,7 @@
v-model:width="width"
v-model:height="editorHeight"
:title="fieldTitle"
:framework-width="frameworkWidth"
:position="boxPosition"
>
<template #body>
@ -34,6 +35,7 @@
v-model:width="width"
v-model:height="editorHeight"
title="快速添加数据定义"
:framework-width="frameworkWidth"
:position="boxPosition"
>
<template #body>
@ -360,6 +362,7 @@ const { height: editorHeight } = useEditorContentHeight();
const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null));
const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating);
const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
/**
* DataSourceConfigPanel 注入打开数据源详情后需要直接打开的字段路径字段名数组

View File

@ -12,6 +12,7 @@
v-model:width="width"
v-model:height="editorHeight"
:title="drawerTitle"
:framework-width="frameworkWidth"
:position="boxPosition"
>
<template #body>
@ -255,4 +256,5 @@ const addDialogVisible = defineModel<boolean>('visible', { default: false });
const { height: editorHeight } = useEditorContentHeight();
const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null));
const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating);
const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
</script>

View File

@ -132,6 +132,7 @@
v-model:height="columnLeftHeight"
:width="columnLeftWidth"
:title="config.text"
:framework-width="frameworkWidth"
:position="{
left: floatBoxStates[config.$key].left,
top: floatBoxStates[config.$key].top,
@ -221,6 +222,7 @@ const taskLength = computed(() => depService.get('taskLength'));
const tipsBarVisible = ref(true);
const columnLeftWidth = computed(() => uiService.get('columnWidth')[ColumnLayout.LEFT]);
const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
const { height: editorContentHeight } = useEditorContentHeight();
const columnLeftHeight = ref(0);

View File

@ -5,6 +5,7 @@
v-model:width="width"
v-model:height="editorHeight"
:title="title"
:framework-width="frameworkWidth"
:position="boxPosition"
>
<template #body>
@ -66,6 +67,7 @@ const { height: editorHeight } = useEditorContentHeight();
const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null));
const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating);
const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
/** 供「方法定义」tab 内的字段消费,用于打开数据源详情后自动打开指定方法 */
provide(

View File

@ -8,6 +8,7 @@
ref="box"
v-model:visible="visible"
title="当前位置下的组件"
:framework-width="frameworkWidth"
:position="menuPosition"
>
<template #body>
@ -36,7 +37,9 @@ import { useNodeStatus } from '@editor/layouts/sidebar/layer/use-node-status';
import type { TreeNodeData } from '@editor/type';
const services = useServices();
const { editorService } = services;
const { editorService, uiService } = services;
const frameworkWidth = computed(() => uiService.get('frameworkRect')?.width || 0);
const visible = ref(false);
const buttonVisible = ref(false);

View File

@ -64,6 +64,18 @@ const services = {
},
};
const rect200: DOMRect = {
x: 0,
y: 0,
width: 200,
height: 100,
top: 0,
left: 0,
right: 200,
bottom: 100,
toJSON: () => '',
};
describe('FloatingBox.vue', () => {
beforeEach(() => {
moveableHandlers.clear();
@ -157,14 +169,61 @@ describe('FloatingBox.vue', () => {
test('left + width 超过 frameworkWidth 时 left 被收敛', async () => {
const wrapper = mount(FloatingBox as any, {
...services,
props: { visible: true, position: { left: 950, top: 0 }, width: 200 },
props: { visible: true, position: { left: 950, top: 0 }, width: 200, frameworkWidth: 1000 },
attachTo: document.body,
});
await new Promise((r) => setTimeout(r, 0));
await wrapper.vm.$nextTick();
const box = document.querySelector('.m-editor-float-box') as HTMLElement;
expect(box).not.toBeNull();
// jsdom 中 getBoundingClientRect 返回 0width 会被重置为 0故不触发收敛left 保持原值
expect(box.style.left).toBe('950px');
wrapper.unmount();
});
test('当实际宽度使 left + width 超过 frameworkWidth 时 left 收敛到右边界内', async () => {
const getBoundingClientRectSpy = vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(rect200);
const wrapper = mount(FloatingBox as any, {
props: { visible: true, position: { left: 950, top: 0 }, frameworkWidth: 1000 },
attachTo: document.body,
});
await new Promise((r) => setTimeout(r, 0));
await wrapper.vm.$nextTick();
const box = document.querySelector('.m-editor-float-box') as HTMLElement;
expect(box).not.toBeNull();
expect(box.style.left).toBe('800px');
getBoundingClientRectSpy.mockRestore();
wrapper.unmount();
});
test('未传入 frameworkWidth 时默认按视窗宽度收敛 left', async () => {
const getBoundingClientRectSpy = vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(rect200);
const innerWidthSpy = vi.spyOn(globalThis, 'window', 'get').mockReturnValue({ innerWidth: 300 } as any);
const wrapper = mount(FloatingBox as any, {
props: { visible: true, position: { left: 250, top: 0 } },
attachTo: document.body,
});
await new Promise((r) => setTimeout(r, 0));
await wrapper.vm.$nextTick();
const box = document.querySelector('.m-editor-float-box') as HTMLElement;
expect(box).not.toBeNull();
// 250 + 200 = 450 > 视窗宽度 300left 收敛为 300 - 200 = 100
expect(box.style.left).toBe('100px');
getBoundingClientRectSpy.mockRestore();
innerWidthSpy.mockRestore();
wrapper.unmount();
});
test('传入 initialStyle 时合并到浮窗样式', async () => {
const wrapper = mount(FloatingBox as any, {
props: { visible: true, initialStyle: { backgroundColor: 'rgb(255, 0, 0)' } },
attachTo: document.body,
});
await new Promise((r) => setTimeout(r, 0));
await wrapper.vm.$nextTick();
const box = document.querySelector('.m-editor-float-box') as HTMLElement;
expect(box).not.toBeNull();
expect(box.style.backgroundColor).toBe('rgb(255, 0, 0)');
wrapper.unmount();
});

View File

@ -38,10 +38,14 @@ const editorService = {
select: vi.fn(),
};
const uiService = {
get: vi.fn((k: string) => (k === 'frameworkRect' ? { width: 1000 } : undefined)),
};
const nodeStatusMap = ref(new Map<string, any>([['p1', { selected: false }]]));
vi.mock('@editor/hooks/use-services', () => ({
useServices: () => ({ editorService }),
useServices: () => ({ editorService, uiService }),
}));
vi.mock('@editor/layouts/sidebar/layer/use-node-status', () => ({