mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-07-24 23:18:07 +00:00
Compare commits
No commits in common. "master" and "v1.8.0-beta.13" have entirely different histories.
master
...
v1.8.0-bet
40
CHANGELOG.md
40
CHANGELOG.md
@ -1,43 +1,3 @@
|
||||
# [1.8.0-beta.16](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.15...v1.8.0-beta.16) (2026-07-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 参考建议可选值最多展示个数由 5 调整为 20 ([0e1986c](https://github.com/Tencent/tmagic-editor/commit/0e1986c81998e50461c294c1a27a8139e8ae9a13))
|
||||
# [1.8.0-beta.15](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.14...v1.8.0-beta.15) (2026-07-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **core:** 为背景图片 URL 添加引号 ([97e0cac](https://github.com/Tencent/tmagic-editor/commit/97e0cac44a2d6ceafaf95fbc4ae716bfe5d47f70))
|
||||
* **editor:** 使用属性面板中的源码删除样式无效 ([cde0805](https://github.com/Tencent/tmagic-editor/commit/cde08052924ddef1c51faeb68ce15d3195827adb))
|
||||
* **editor:** 右键菜单动态变化后修复菜单位置防止截断 ([6919a0a](https://github.com/Tencent/tmagic-editor/commit/6919a0a0b19efff92821fc8d52b61b2f7382a817))
|
||||
* 跳过无字段名称配置的类型校验 ([11bf037](https://github.com/Tencent/tmagic-editor/commit/11bf037054774937132743a40006364e49ee8fd5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **design:** 优化 FormItem 和 colorPicker 样式 ([2e031cc](https://github.com/Tencent/tmagic-editor/commit/2e031cc1346fb6ea739f0d45a89213b6577dc7ee))
|
||||
* **editor:** 数据源字段选择器校验提示细化并支持 fieldConfig 类型校验 ([fe9e754](https://github.com/Tencent/tmagic-editor/commit/fe9e754b50f14e6034170b2284553ec1c4536150))
|
||||
* **form,design:** 一行多个表单默认右靠齐label ([48abab7](https://github.com/Tencent/tmagic-editor/commit/48abab7051b93b7ed336172c21ab3b908733f2e7))
|
||||
* **form,editor:** submitForm/validateForm 静默挂载时注入静默标记,vs-code 字段跳过 monaco 渲染 ([3b9d5da](https://github.com/Tencent/tmagic-editor/commit/3b9d5da0bc9f323e944c0e2adb4fc88642ce1915))
|
||||
* **form:** text 类型校验允许数字值,兼容数据源字段配置 ([1f389ff](https://github.com/Tencent/tmagic-editor/commit/1f389ff9fd77fb93ad749f3fdbbc8aa0fe8725ac))
|
||||
* 新增 page-structure-change 事件,支持页面增删统一通知 ([745f449](https://github.com/Tencent/tmagic-editor/commit/745f44939605528e8e2ccd0639478db98293a441))
|
||||
# [1.8.0-beta.14](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.13...v1.8.0-beta.14) (2026-07-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **editor:** 保留历史操作来源并补发插入事件 ([89689bb](https://github.com/Tencent/tmagic-editor/commit/89689bb9fd2d2d3edcc027b87932cbdd3a5755ad))
|
||||
* **form:** 统一 extendState 合并逻辑 ([0feada8](https://github.com/Tencent/tmagic-editor/commit/0feada8d1fa4057444da746040b3824df09eef07))
|
||||
* **stage:** 修复选中组件时 stage 外层滚动 ([09eecd5](https://github.com/Tencent/tmagic-editor/commit/09eecd5bedd49e5a002fa5ef122eaa1579dc895b))
|
||||
* 调整编辑器配置项展示 ([8699dc1](https://github.com/Tencent/tmagic-editor/commit/8699dc12af9d52b1c460f61f3d3f48e4cf3fc5f6))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **design,editor:** 新增magic-admin主题样式,优化组件表单配置布局样式 ([b13cd34](https://github.com/Tencent/tmagic-editor/commit/b13cd3425b16eaeac33f897bf51e80f7984db782))
|
||||
* playground 支持主题切换 ([ee5247c](https://github.com/Tencent/tmagic-editor/commit/ee5247ca9436c385a5b634d161726006912fe77c))
|
||||
# [1.8.0-beta.13](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.11...v1.8.0-beta.13) (2026-07-22)
|
||||
|
||||
|
||||
|
||||
@ -162,7 +162,7 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary,#409eff);;
|
||||
color: #409eff;
|
||||
background-color: #f9fafc;
|
||||
}
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ export default defineConfig([
|
||||
'*/**/public/**/*',
|
||||
'*/**/types/**/*',
|
||||
'*/**/*.config.ts',
|
||||
'./temp/**/*',
|
||||
'./tepm/**/*',
|
||||
'vite-env.d.ts',
|
||||
]),
|
||||
...eslintConfig(path.join(path.dirname(fileURLToPath(import.meta.url)), 'tsconfig.json')),
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.16",
|
||||
"version": "1.8.0-beta.13",
|
||||
"name": "tmagic",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.16",
|
||||
"version": "1.8.0-beta.13",
|
||||
"name": "@tmagic/cli",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.16",
|
||||
"version": "1.8.0-beta.13",
|
||||
"name": "@tmagic/core",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -51,7 +51,7 @@ export const style2Obj = (style: string) => {
|
||||
|
||||
export const fillBackgroundImage = (value: string) => {
|
||||
if (value && !/^url/.test(value) && !/^linear-gradient/.test(value)) {
|
||||
return `url("${value}")`;
|
||||
return `url(${value})`;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
@ -50,7 +50,7 @@ describe('style2Obj', () => {
|
||||
|
||||
describe('fillBackgroundImage', () => {
|
||||
test('裸路径会包裹 url()', () => {
|
||||
expect(fillBackgroundImage('a.png')).toBe('url("a.png")');
|
||||
expect(fillBackgroundImage('a.png')).toBe('url(a.png)');
|
||||
});
|
||||
|
||||
test('已经是 url() 不重复包裹', () => {
|
||||
@ -127,7 +127,7 @@ describe('transformStyle', () => {
|
||||
|
||||
test('backgroundImage: browser 下补全 url()', () => {
|
||||
expect(transformStyle({ backgroundImage: 'a.png' }, 'browser')).toEqual({
|
||||
backgroundImage: 'url("a.png")',
|
||||
backgroundImage: 'url(a.png)',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.16",
|
||||
"version": "1.8.0-beta.13",
|
||||
"name": "@tmagic/data-source",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.16",
|
||||
"version": "1.8.0-beta.13",
|
||||
"name": "@tmagic/dep",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.16",
|
||||
"version": "1.8.0-beta.13",
|
||||
"name": "@tmagic/design",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
|
||||
@ -1,23 +0,0 @@
|
||||
<template>
|
||||
<component class="tmagic-design-alert" :is="uiComponent" v-bind="uiProps">
|
||||
<slot></slot>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { getDesignConfig } from './config';
|
||||
import type { AlertProps } from './types';
|
||||
|
||||
defineOptions({
|
||||
name: 'TMAlert',
|
||||
});
|
||||
|
||||
const props = defineProps<AlertProps>();
|
||||
|
||||
const ui = getDesignConfig('components')?.alert;
|
||||
const uiComponent = ui?.component || 'el-alert';
|
||||
|
||||
const uiProps = computed<AlertProps>(() => ui?.props(props) || props);
|
||||
</script>
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<component :is="uiComponent" :class="['tmagic-design-card', { 'tmagic-design-card--flat': isFlat }]" v-bind="uiProps">
|
||||
<component class="tmagic-design-card" :is="uiComponent" v-bind="uiProps">
|
||||
<template #header v-if="$slots.header">
|
||||
<slot name="header" class="header"></slot>
|
||||
</template>
|
||||
@ -14,7 +14,6 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { getDesignConfig } from './config';
|
||||
import { isGlobalFlat } from './index';
|
||||
import type { CardProps } from './types';
|
||||
|
||||
defineOptions({
|
||||
@ -27,14 +26,5 @@ const ui = getDesignConfig('components')?.card;
|
||||
|
||||
const uiComponent = ui?.component || 'el-card';
|
||||
|
||||
// 当祖先 `<MEditor>` / `<MForm>` 的 theme 是 `magic-admin` 时,整套设计语言默认走「无卡片感」,
|
||||
// 因此这里自动等价于 `flat=true`;调用方仍可显式传 `flat` 强制开启。
|
||||
const isFlat = computed(() => !!props.flat || isGlobalFlat.value);
|
||||
|
||||
// 把 `flat` 从转发给底层 UI 库(el-card / t-card 等)的 props 中剥离:
|
||||
// 它仅作用于本组件的视觉修饰类,不需要也不应该作为属性落到底层组件 / DOM 上。
|
||||
const uiProps = computed<CardProps>(() => {
|
||||
const { flat: _flat, ...rest } = props;
|
||||
return ui?.props(rest as CardProps) || (rest as CardProps);
|
||||
});
|
||||
const uiProps = computed<CardProps>(() => ui?.props(props) || props);
|
||||
</script>
|
||||
|
||||
@ -7,39 +7,18 @@
|
||||
@update:modelValue="updateModelValue"
|
||||
>
|
||||
</component>
|
||||
<MInput
|
||||
v-if="isFlat && !isLargeStageContainer"
|
||||
@update:modelValue="updateModelValue"
|
||||
@change="changeHandler"
|
||||
v-bind="uiProps"
|
||||
class="tmagic-design-color-picker-input"
|
||||
clearable
|
||||
></MInput>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, type ComputedRef, inject } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { getDesignConfig } from './config';
|
||||
import { isGlobalFlat } from './index';
|
||||
import MInput from './Input.vue';
|
||||
import type { ColorPickerProps } from './types';
|
||||
|
||||
defineOptions({
|
||||
name: 'TMColorPicker',
|
||||
});
|
||||
|
||||
const isFlat = computed(() => !!props.flat || isGlobalFlat.value);
|
||||
|
||||
// 祖先 `<MEditor>` 用 computed 包着 provide,值会随 props.isLargeStageContainer 变化。
|
||||
// 类型标成 `ComputedRef<boolean>`,模板里 `<script setup>` 自动解包,仍可直接写
|
||||
// `!isLargeStageContainer`;默认值也得包成 computed,保持形状一致,避免祖先没提供时
|
||||
// 模板里对一个裸 `false` 和 `ComputedRef` 走两条不同的取值路径。
|
||||
const isLargeStageContainer = inject<ComputedRef<boolean>>(
|
||||
'isLargeStageContainer',
|
||||
computed(() => false),
|
||||
);
|
||||
|
||||
const props = withDefaults(defineProps<ColorPickerProps>(), {
|
||||
showAlpha: false,
|
||||
disabled: false,
|
||||
|
||||
@ -1,24 +1,12 @@
|
||||
<template>
|
||||
<component
|
||||
class="tmagic-design-form-item"
|
||||
:class="{ 'has-extra-tips': adapterType === 'element-plus' && extraTips }"
|
||||
:is="uiComponent"
|
||||
v-bind="uiProps"
|
||||
>
|
||||
<component class="tmagic-design-form-item" :is="uiComponent" v-bind="uiProps">
|
||||
<template #label>
|
||||
<slot name="label"> </slot>
|
||||
<slot name="label"></slot>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<slot></slot>
|
||||
<alert
|
||||
v-if="adapterType === 'element-plus' && extraTips"
|
||||
:title="extraTips"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
></alert>
|
||||
<div v-else-if="adapterType === 'element-plus' && extra" v-html="extra" class="m-form-tip"></div>
|
||||
<div v-if="adapterType === 'element-plus' && extra" v-html="extra" class="m-form-tip"></div>
|
||||
</template>
|
||||
|
||||
<template v-if="adapterType === 'element-plus'" #error="{ error }">
|
||||
@ -28,32 +16,26 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import Alert from './Alert.vue';
|
||||
import { getDesignConfig } from './config';
|
||||
import { stripValidateSuggestion } from './formValidateMessage';
|
||||
import { isGlobalFlat } from './index';
|
||||
import type { FormItemProps } from './types';
|
||||
|
||||
defineOptions({
|
||||
name: 'TMFormItem',
|
||||
});
|
||||
|
||||
const props = defineProps<FormItemProps & { theme?: string }>();
|
||||
const props = defineProps<FormItemProps>();
|
||||
|
||||
const ui = getDesignConfig('components')?.formItem;
|
||||
|
||||
const uiComponent = ui?.component || 'el-form-item';
|
||||
|
||||
const adapterType = getDesignConfig('adapterType');
|
||||
|
||||
const formInline = inject<boolean>('formInline', false);
|
||||
const formInRow = inject<boolean>('isInRow', false);
|
||||
const uiProps = computed<FormItemProps>(() => {
|
||||
const { extra, extraTips, ...rest } = ui?.props(props) || props;
|
||||
if (isGlobalFlat.value && rest.labelPosition === undefined) {
|
||||
return { ...rest, labelPosition: formInline || formInRow ? 'right' : 'left' };
|
||||
}
|
||||
const { extra, ...rest } = ui?.props(props) || props;
|
||||
return rest;
|
||||
});
|
||||
|
||||
|
||||
@ -4,9 +4,10 @@
|
||||
<div
|
||||
v-if="popoverVisible || !destroyOnClose"
|
||||
v-show="popoverVisible"
|
||||
class="tmagic-design-popper"
|
||||
ref="popperElementRef"
|
||||
:tabindex="tabindex"
|
||||
:class="['tmagic-design-popper', popperClass, themeClass]"
|
||||
:class="popperClass"
|
||||
:style="style"
|
||||
@mouseenter.once="popperMouseenterHandler"
|
||||
>
|
||||
@ -21,7 +22,7 @@ import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref
|
||||
import type { Instance } from '@popperjs/core';
|
||||
import { createPopper } from '@popperjs/core';
|
||||
|
||||
import { useThemeClass, useZIndex } from './index';
|
||||
import { useZIndex } from './index';
|
||||
import type { PopoverProps } from './types';
|
||||
|
||||
defineSlots<{
|
||||
@ -45,12 +46,6 @@ const props = withDefaults(defineProps<PopoverProps>(), {
|
||||
closeOnClickOutside: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* 主题修饰类(来自最近的 `<MEditor>` / `<MForm>` 祖先 provide)。
|
||||
* 挂在 `Teleport` 出去的 popper 根节点上,让主题级 CSS 变量(`--el-color-primary` 等)
|
||||
* 在 portal 节点上也能命中。详见 `@tmagic/design/theme.ts`。
|
||||
*/
|
||||
const themeClass = useThemeClass();
|
||||
const emit = defineEmits<{
|
||||
/** 受控模式(传入了 visible)下点击外部收起时触发,便于配合 v-model:visible。 */
|
||||
'update:visible': [_visible: boolean];
|
||||
|
||||
@ -8,7 +8,6 @@ import './theme/index.scss';
|
||||
|
||||
export * from './types';
|
||||
export * from './config';
|
||||
export * from './theme';
|
||||
export * from './formValidateMessage';
|
||||
|
||||
export { default as TMagicAutocomplete } from './Autocomplete.vue';
|
||||
@ -95,10 +94,6 @@ export let useZIndex = (zIndexOverrides?: Ref<number>) => {
|
||||
};
|
||||
};
|
||||
|
||||
// 与 form/utils/config、editor/utils/config 里的同名变量保持一致:用 ref 持有 flat 全局开关,
|
||||
// const 引用本身不可变,规避 `no-mutable-exports` / `naming-convention` 对 `let` 模块变量的限制。
|
||||
export const isGlobalFlat = ref(false);
|
||||
|
||||
export default {
|
||||
install(app: App, options: DesignPluginOptions) {
|
||||
tMagicMessage =
|
||||
@ -134,8 +129,6 @@ export default {
|
||||
useZIndex = options.useZIndex;
|
||||
}
|
||||
|
||||
isGlobalFlat.value = options.flat ?? false;
|
||||
|
||||
if (options.adapterType && globalThis.document?.documentElement) {
|
||||
globalThis.document.documentElement.classList.add(`tmagic-adapter-${options.adapterType}`);
|
||||
}
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
/*
|
||||
* 主题作用域穿透到 portal 节点的基建。
|
||||
*
|
||||
* `<MEditor>` / `<MForm>` 通过 provide 暴露当前的 `theme`,包含 `<Teleport>` 的组件
|
||||
* 通过 `useThemeClass()` inject 得到主题修饰类,挂到传送目标根节点上即可让主题级
|
||||
* CSS 变量(如 `--el-color-primary`)在 body 下的 portal 节点上也命中。
|
||||
*
|
||||
* 命名约定:portal 节点本身既不是 `m-editor` 也不是 `m-form`,因此使用独立的中性类
|
||||
* `m-theme--<theme>`,避免语义混淆。同时把跨组件共享的主题级 CSS 变量统一挂在该类上,
|
||||
* editor 根 / form 根 / portal 根都加上该类即可统一受用,editor / form 各自的内部
|
||||
* 样式仍保留在 `.m-editor.m-editor--<theme>` / `.m-form.m-form--<theme>` 双类选择器
|
||||
* 上,互不污染。
|
||||
*/
|
||||
|
||||
import type { ComputedRef, InjectionKey, Ref } from 'vue';
|
||||
import { computed, inject } from 'vue';
|
||||
|
||||
/** 祖先 `<MEditor>` / `<MForm>` 暴露的当前 theme(一般来自 props.theme,经 MForm 时
|
||||
* 若自身没设 theme,会透传外层 `<MEditor>` 的值,详见 Form.vue 中的 effectiveTheme)。 */
|
||||
export const M_THEME_KEY: InjectionKey<Ref<string>> = Symbol('mTheme');
|
||||
|
||||
/**
|
||||
* 取得来自最近 `<MEditor>` / `<MForm>` 祖先 provide 的主题,拼成可直接套到 class 字符串
|
||||
* 的格式:例如 `"m-theme--magic-admin"`。
|
||||
*
|
||||
* - 不在主题作用域里时返回空串,调用方可直接拼接,无需额外判空。
|
||||
* - 与 `m-editor` / `m-form` 同名修饰类完全解耦:portal 节点只挂这一个中性主题类,
|
||||
* 不会意外命中 editor / form 内部 DOM 的样式作用域。
|
||||
*
|
||||
* 使用场景:组件自身渲染了 `<Teleport>`(如 `TMagicPopover` / `FloatingBox` /
|
||||
* `ContentMenu`),需要在传送目标根节点上挂主题类,让主题 CSS 变量能命中。
|
||||
*/
|
||||
export const useThemeClass = (): ComputedRef<string> => {
|
||||
const theme = inject(M_THEME_KEY, null);
|
||||
return computed(() => {
|
||||
const t = theme?.value;
|
||||
return t ? `m-theme--${t}` : '';
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 在组件内统一解析当前生效的 theme:以 `prop.theme` 为准,缺省时回退到祖先
|
||||
* `<MEditor>` / `<MForm>` provide 的主题。
|
||||
*
|
||||
* 入参 `prop` 是任意结构,约束只看 `theme?: string`。一般直接把 `defineProps()` 的
|
||||
* 返回值传进来即可(reactive proxy,computed 可正确跟踪外部更新);不要传
|
||||
* `toRefs` 解构后的对象。
|
||||
*
|
||||
* 若 `prop.theme` 与祖先都没有,返回空串,调用方可直接拼接。
|
||||
*
|
||||
* @example
|
||||
* const props = defineProps<{ theme?: string }>();
|
||||
* const theme = useTheme(props);
|
||||
* // theme.value -> 'magic-admin' 或 ''
|
||||
*/
|
||||
export const useTheme = <T extends { theme?: string }>(prop: T): ComputedRef<string> => {
|
||||
const injected = inject(M_THEME_KEY, null);
|
||||
return computed(() => prop.theme || injected?.value || '');
|
||||
};
|
||||
@ -1,67 +0,0 @@
|
||||
// `TMagicCard` 的「铺平」模式:去掉卡片自身的视觉表达(背景 / 阴影 / 边框 / 圆角 /
|
||||
// header 分隔线 / header & body 内边距),让卡片在视觉上与外层容器融为一体。
|
||||
//
|
||||
// 不动 body-style、不动折叠相关 prop —— 这些上层用法仍可正常工作。
|
||||
// `is-always-shadow` 是 el-card 在 `shadow="always"` 时附加的类,需要单独再压一次。
|
||||
.tmagic-design-card.tmagic-design-card--flat {
|
||||
background: #fff;
|
||||
box-shadow: none !important;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-radius: 0;
|
||||
border-bottom: 0;
|
||||
|
||||
&.m-fields-group-list-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&.is-always-shadow {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.el-card__header {
|
||||
border-bottom: none;
|
||||
padding: 16px;
|
||||
&:hover {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
padding: 0 16px 0 16px;
|
||||
// 这里每个form-item应该要有一个16px间距,但是除了tbody中的form-item
|
||||
.tmagic-design-form-item:not(tbody .tmagic-design-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
> .m-container-row {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
.tmagic-design-card--flat {
|
||||
// .el-card__body {
|
||||
// padding: 0;
|
||||
// }
|
||||
// .el-card__header {
|
||||
// padding: 0 16px;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
.m-split-title-card-container {
|
||||
> .tmagic-design-card.tmagic-design-card--flat {
|
||||
background-color: transparent;
|
||||
> .el-card__header {
|
||||
margin-bottom: 10px;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
> .el-card__body {
|
||||
background-color: #fff;
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
.tmagic-design-color-picker-input {
|
||||
margin-left: 8px;
|
||||
width: calc(var(--el-input-width, 100%) - 40px);
|
||||
}
|
||||
|
||||
.tmagic-design-color-picker {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
@ -1,3 +1 @@
|
||||
@use "./popover.scss";
|
||||
@use "./card.scss";
|
||||
@use "./colorPicker.scss";
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
@use "../../index.scss";
|
||||
|
||||
.m-form.m-form--magic-admin {
|
||||
.el-collapse-item__header {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
padding-left: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.tmagic-design-form-item {
|
||||
&.has-extra-tips {
|
||||
> .el-form-item__content {
|
||||
gap: 10px;
|
||||
> .tmagic-design-alert {
|
||||
border-radius: 2px;
|
||||
width: fit-content;
|
||||
padding: 0 8px;
|
||||
color: var(--el-color-primary, #0056ea);
|
||||
background-color: var(--el-color-primary-light-9, rgb(236, 245, 255));
|
||||
.el-icon.el-alert__icon {
|
||||
fill: var(--el-color-primary, #0056ea);
|
||||
color: var(--el-color-primary, #0056ea);
|
||||
}
|
||||
.el-alert__title {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -24,17 +24,6 @@ export interface BadgeProps {
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export interface AlertProps {
|
||||
title?: string;
|
||||
/** ElAlert 实际支持的 type;与 ButtonProps.type 不同,没有 `primary` / `danger` */
|
||||
type?: 'success' | 'warning' | 'info' | 'error' | 'primary';
|
||||
description?: string;
|
||||
closable?: boolean;
|
||||
center?: boolean;
|
||||
closeText?: string;
|
||||
showIcon?: boolean;
|
||||
effect?: 'light' | 'dark';
|
||||
}
|
||||
export interface ButtonProps {
|
||||
type?: string;
|
||||
size?: FieldSize;
|
||||
@ -50,11 +39,6 @@ export interface CardProps {
|
||||
bodyStyle?: Record<string, any>;
|
||||
shadow?: string;
|
||||
header?: string;
|
||||
/**
|
||||
* 铺平模式:去除卡片自身的视觉(背景 / 阴影 / 边框 / 圆角 / header 分隔线 / header 与 body 内边距)。
|
||||
* 用于已有外层容器或者使用方希望「无卡片感」的场景,TMagicCard 的折叠等 prop 仍可正常使用。
|
||||
*/
|
||||
flat?: boolean;
|
||||
}
|
||||
|
||||
export interface CascaderProps {
|
||||
@ -113,7 +97,6 @@ export interface ColorPickerProps {
|
||||
disabled?: boolean;
|
||||
showAlpha?: boolean;
|
||||
size?: FieldSize;
|
||||
flat?: boolean;
|
||||
}
|
||||
|
||||
export interface DatePickerProps {
|
||||
@ -202,9 +185,7 @@ export interface FormItemProps {
|
||||
labelWidth?: string | number;
|
||||
rules?: any;
|
||||
extra?: string;
|
||||
extraTips?: string;
|
||||
labelPosition?: 'top' | 'left' | 'right';
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface InputProps {
|
||||
@ -483,11 +464,6 @@ export interface Components {
|
||||
props: (props: BadgeProps) => BadgeProps;
|
||||
};
|
||||
|
||||
alert: {
|
||||
component: DefineComponent<AlertProps, {}, any> | string;
|
||||
props: (props: AlertProps) => AlertProps;
|
||||
};
|
||||
|
||||
autocomplete: {
|
||||
component: DefineComponent<AutocompleteProps, {}, any> | string;
|
||||
props: (props: AutocompleteProps) => AutocompleteProps;
|
||||
@ -750,7 +726,6 @@ export interface Components {
|
||||
}
|
||||
|
||||
export interface DesignPluginOptions {
|
||||
flat?: boolean;
|
||||
adapterType?: string;
|
||||
message?: TMagicMessage;
|
||||
messageBox?: TMagicMessageBox;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.16",
|
||||
"version": "1.8.0-beta.13",
|
||||
"name": "@tmagic/editor",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
:page-bar-sort-options="pageBarSortOptions"
|
||||
:page-filter-function="pageFilterFunction"
|
||||
:hide-sidebar="hideSidebar"
|
||||
:theme="theme"
|
||||
>
|
||||
<template #header>
|
||||
<slot name="header"></slot>
|
||||
@ -136,10 +135,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { computed, provide, ref } from 'vue';
|
||||
import { provide, ref } from 'vue';
|
||||
|
||||
import type { MApp } from '@tmagic/core';
|
||||
import { M_THEME_KEY } from '@tmagic/design';
|
||||
|
||||
import Framework from './layouts/Framework.vue';
|
||||
import TMagicNavMenu from './layouts/NavMenu.vue';
|
||||
@ -243,15 +241,6 @@ provide(ENABLE_PROPS_FORM_VALIDATE, props.enablePropsFormValidate ?? false);
|
||||
* 与 PropsPanel 通过 `:extend-state` 显式传入的方式保持等价。
|
||||
*/
|
||||
provide('extendFormState', props.extendFormState);
|
||||
|
||||
// 用 computed 包一层再 provide,否则传下去的是 provide 那一刻的值快照,
|
||||
// props.isLargeStageContainer 后续变化不会同步到子孙(如 @tmagic/design/ColorPicker)。
|
||||
// 子孙侧 `inject<ComputedRef<boolean>>('isLargeStageContainer', computed(() => false))`
|
||||
// 用 `.value` 或模板自动解包读取即可拿到最新值。
|
||||
provide(
|
||||
'isLargeStageContainer',
|
||||
computed(() => props.isLargeStageContainer),
|
||||
);
|
||||
/**
|
||||
* 提供 PropsPanel 主属性表单的 formState getter,供历史差异弹窗复用,
|
||||
* 让 CompareForm 与 PropsPanel 的 filterFunction 上下文保持一致。
|
||||
@ -267,16 +256,6 @@ provide('historyListExtraTabs', props.historyListExtraTabs);
|
||||
|
||||
provide<EventBus>('eventBus', new EventEmitter());
|
||||
|
||||
/**
|
||||
* 把当前主题以响应式 ref 形式 provide 给后代,供包含 `Teleport` 的组件
|
||||
* (如 `TMagicPopover` / `FloatingBox` / `ContentMenu`)在传送目标上挂 `m-theme--<theme>`
|
||||
* 类,让主题级 CSS 变量在 portal 节点上也能命中。详见 `@tmagic/design/theme.ts`。
|
||||
*/
|
||||
provide(
|
||||
M_THEME_KEY,
|
||||
computed(() => props.theme ?? ''),
|
||||
);
|
||||
|
||||
const propsPanelMountedHandler = (e: InstanceType<typeof FormPanel>) => {
|
||||
propsPanelRef.value = e;
|
||||
emit('props-panel-mounted', e);
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-show="visible"
|
||||
class="magic-editor-content-menu"
|
||||
ref="menu"
|
||||
:class="['magic-editor-content-menu', themeClass]"
|
||||
:style="menuStyle"
|
||||
@mouseenter="mouseenterHandler()"
|
||||
@contextmenu.prevent
|
||||
@ -39,7 +39,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, type Ref, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { useThemeClass, useZIndex } from '@tmagic/design';
|
||||
import { useZIndex } from '@tmagic/design';
|
||||
|
||||
import { MenuButton, MenuComponent } from '@editor/type';
|
||||
|
||||
@ -73,14 +73,6 @@ const menuEl = useTemplateRef<HTMLDivElement>('menu');
|
||||
const buttonRefs = useTemplateRef<InstanceType<typeof ToolButton>[]>('buttons');
|
||||
const subMenuRef = useTemplateRef<any>('subMenu');
|
||||
const visible = ref(false);
|
||||
|
||||
/**
|
||||
* 主题修饰类(来自最近的 `<MEditor>` / `<MForm>` 祖先 provide)。
|
||||
* - 顶层 `ContentMenu` 渲染在编辑器子树中,加上去是冗余但无害;
|
||||
* - 子菜单(递归 `<content-menu :is-sub-menu="true">` 被 `<teleport to="body">` 送到 body)
|
||||
* 不在编辑器子树中,必须显式挂上主题类,主题级 CSS 变量才能命中。
|
||||
*/
|
||||
const themeClass = useThemeClass();
|
||||
const subMenuData: Ref<(MenuButton | MenuComponent)[]> = ref<(MenuButton | MenuComponent)[]>([]);
|
||||
const zIndex = useZIndex();
|
||||
const curZIndex = ref<number>(0);
|
||||
@ -128,42 +120,20 @@ const outsideClickHideHandler = (e: MouseEvent) => {
|
||||
hide();
|
||||
};
|
||||
|
||||
// 根据菜单实际尺寸修正位置,避免超出可视范围
|
||||
const fixPosition = () => {
|
||||
const menu = menuEl.value;
|
||||
if (!menu || !visible.value) return;
|
||||
|
||||
const menuHeight = menu.clientHeight;
|
||||
const menuWidth = menu.clientWidth;
|
||||
|
||||
let { top, left } = menuPosition.value;
|
||||
|
||||
if (top + menuHeight > document.body.clientHeight) {
|
||||
top = Math.max(0, document.body.clientHeight - menuHeight);
|
||||
}
|
||||
|
||||
if (left + menuWidth > document.body.clientWidth) {
|
||||
left = Math.max(0, document.body.clientWidth - menuWidth);
|
||||
}
|
||||
|
||||
if (top !== menuPosition.value.top || left !== menuPosition.value.left) {
|
||||
menuPosition.value = { top, left };
|
||||
}
|
||||
};
|
||||
|
||||
const setPosition = (e: { clientY: number; clientX: number }) => {
|
||||
const menuHeight = menuEl.value?.clientHeight || 0;
|
||||
|
||||
let top = e.clientY;
|
||||
if (menuHeight + e.clientY > document.body.clientHeight) {
|
||||
top = document.body.clientHeight - menuHeight;
|
||||
}
|
||||
|
||||
menuPosition.value = {
|
||||
top: e.clientY,
|
||||
top,
|
||||
left: e.clientX,
|
||||
};
|
||||
fixPosition();
|
||||
};
|
||||
|
||||
// 菜单大小动态变化(如菜单项更新)后重新修正位置
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
fixPosition();
|
||||
});
|
||||
|
||||
const show = (e?: { clientY: number; clientX: number }) => {
|
||||
visible.value = true;
|
||||
|
||||
@ -208,18 +178,12 @@ const mouseenterHandler = () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (menuEl.value) {
|
||||
resizeObserver.observe(menuEl.value);
|
||||
}
|
||||
|
||||
if (props.isSubMenu) return;
|
||||
|
||||
globalThis.addEventListener('mousedown', outsideClickHideHandler, true);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver.disconnect();
|
||||
|
||||
if (props.isSubMenu) return;
|
||||
|
||||
globalThis.removeEventListener('mousedown', outsideClickHideHandler, true);
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<Teleport to="body" v-if="visible">
|
||||
<div
|
||||
ref="target"
|
||||
:class="['m-editor-float-box', themeClass]"
|
||||
class="m-editor-float-box"
|
||||
v-bind="$attrs"
|
||||
:style="{ ...style, zIndex: curZIndex }"
|
||||
@mousedown="nextZIndex"
|
||||
@ -27,7 +27,7 @@ import { computed, type CSSProperties, nextTick, onBeforeUnmount, provide, ref,
|
||||
import { Close } from '@element-plus/icons-vue';
|
||||
import VanillaMoveable from 'moveable';
|
||||
|
||||
import { TMagicButton, useThemeClass, useZIndex } from '@tmagic/design';
|
||||
import { TMagicButton, useZIndex } from '@tmagic/design';
|
||||
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
|
||||
@ -62,12 +62,6 @@ const props = withDefaults(
|
||||
const targetEl = useTemplateRef<HTMLDivElement>('target');
|
||||
const titleEl = useTemplateRef<HTMLDivElement>('title');
|
||||
|
||||
/**
|
||||
* 主题修饰类(来自最近的 `<MEditor>` / `<MForm>` 祖先 provide)。
|
||||
* 挂在 `Teleport` 出去的浮动面板根节点上,让主题级 CSS 变量在 portal 节点上也能命中。
|
||||
*/
|
||||
const themeClass = useThemeClass();
|
||||
|
||||
const zIndex = useZIndex();
|
||||
const curZIndex = ref<number>(0);
|
||||
|
||||
|
||||
@ -35,10 +35,6 @@ import type {
|
||||
export const ENABLE_PROPS_FORM_VALIDATE: InjectionKey<boolean> = Symbol('enablePropsFormValidate');
|
||||
|
||||
export interface EditorProps {
|
||||
/** 是否是大屏模拟器容器,大屏容器时,左侧属性面板会扩展为两列(正常3列),颜色表单不扩展两列等 */
|
||||
isLargeStageContainer?: boolean;
|
||||
|
||||
theme?: string;
|
||||
/** 页面初始值 */
|
||||
modelValue?: MApp;
|
||||
/** 左侧面板中的组件类型列表 */
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<MagicCodeEditor
|
||||
v-if="!silentMode"
|
||||
:height="config.height"
|
||||
:type="diffMode ? 'diff' : undefined"
|
||||
:init-values="diffMode ? (lastValues || {})[name] : model[name]"
|
||||
@ -18,9 +17,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { type CodeConfig, type FieldProps, FORM_SILENT_MODE_KEY } from '@tmagic/form';
|
||||
import type { CodeConfig, FieldProps } from '@tmagic/form';
|
||||
|
||||
import MagicCodeEditor from '@editor/layouts/CodeEditor.vue';
|
||||
|
||||
@ -28,13 +27,6 @@ defineOptions({
|
||||
name: 'MFieldsVsCode',
|
||||
});
|
||||
|
||||
/**
|
||||
* 静默模式(submitForm/validateForm 隐藏挂载)下跳过 monaco 渲染:
|
||||
* 校验由 FormItem 针对 model 中的值完成,与编辑器实例无关;本组件挂载无值副作用
|
||||
* (save 仅由用户操作/编辑器内容变更触发),跳过可省去 monaco worker/model 的无谓实例化。
|
||||
*/
|
||||
const silentMode = inject(FORM_SILENT_MODE_KEY, false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [value: string | any];
|
||||
}>();
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<div class="m-fields-code-select" :class="config.className">
|
||||
<TMagicCard :flat="config.flat">
|
||||
<TMagicCard>
|
||||
<MContainer
|
||||
:config="codeConfig"
|
||||
:size="size"
|
||||
class="code-select-content"
|
||||
:prop="prop"
|
||||
:disabled="disabled"
|
||||
:is-compare="isCompareMode"
|
||||
@ -13,20 +12,16 @@
|
||||
@change="changeHandler"
|
||||
>
|
||||
</MContainer>
|
||||
<TMagicButton class="create-button fullWidth" :icon="Plus" :size="size" :disabled="disabled" @click="newHandler()"
|
||||
>添加{{ config.text }}</TMagicButton
|
||||
>
|
||||
</TMagicCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from 'vue';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
|
||||
import { HookCodeType, HookType } from '@tmagic/core';
|
||||
import { TMagicButton, TMagicCard } from '@tmagic/design';
|
||||
import { TMagicCard } from '@tmagic/design';
|
||||
import type { CodeSelectConfig, ContainerChangeEventData, FieldProps, GroupListConfig } from '@tmagic/form';
|
||||
import { MContainer } from '@tmagic/form';
|
||||
|
||||
@ -58,23 +53,12 @@ const props = withDefaults(defineProps<FieldProps<CodeSelectConfig>>(), {});
|
||||
* 仅当存在历史值时才启用对比,避免 lastValues 缺失时退化为「全部新增」的空对比。
|
||||
*/
|
||||
const isCompareMode = computed(() => Boolean(props.isCompare && props.lastValues));
|
||||
const newHandler = () => {
|
||||
const defaultCode = {
|
||||
codeType: HookCodeType.CODE,
|
||||
codeId: '',
|
||||
};
|
||||
const name = props.config.name || '';
|
||||
const hookData = props.model[name]?.hookData || [];
|
||||
emit('change', defaultCode, {
|
||||
modifyKey: `hookData.${hookData.length}`,
|
||||
});
|
||||
};
|
||||
|
||||
const codeConfig = computed<GroupListConfig>(() => ({
|
||||
type: 'group-list',
|
||||
name: 'hookData',
|
||||
enableToggleMode: false,
|
||||
expandAll: true,
|
||||
addable: () => false,
|
||||
title: (mForm, { model, index }: any) => {
|
||||
if (model.codeType === HookCodeType.DATA_SOURCE_METHOD) {
|
||||
if (Array.isArray(model.codeId)) {
|
||||
@ -97,44 +81,49 @@ const codeConfig = computed<GroupListConfig>(() => ({
|
||||
|
||||
return model.codeId || index;
|
||||
},
|
||||
titlePrefix: props.config.name === undefined ? undefined : String(props.config.name),
|
||||
items: [
|
||||
{
|
||||
text: '代码类型',
|
||||
type: 'select',
|
||||
name: 'codeType',
|
||||
labelPosition: 'right',
|
||||
rules: [{ typeMatch: true, trigger: 'change' }],
|
||||
options: [
|
||||
{ value: HookCodeType.CODE, text: '代码块' },
|
||||
{ value: HookCodeType.DATA_SOURCE_METHOD, text: '数据源方法' },
|
||||
],
|
||||
defaultValue: HookCodeType.CODE,
|
||||
onChange: (_mForm, v: HookCodeType, { setModel }) => {
|
||||
if (v === HookCodeType.DATA_SOURCE_METHOD) {
|
||||
setModel('codeId', []);
|
||||
} else {
|
||||
setModel('codeId', '');
|
||||
}
|
||||
return v;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'code-select-col',
|
||||
name: 'codeId',
|
||||
text: '代码块',
|
||||
rules: [{ typeMatch: true, trigger: 'change' }],
|
||||
type: 'row',
|
||||
items: [
|
||||
{
|
||||
type: 'select',
|
||||
name: 'codeType',
|
||||
span: 6,
|
||||
options: [
|
||||
{ value: HookCodeType.CODE, text: '代码块' },
|
||||
{ value: HookCodeType.DATA_SOURCE_METHOD, text: '数据源方法' },
|
||||
],
|
||||
rules: [{ typeMatch: true, trigger: 'change' }],
|
||||
defaultValue: HookCodeType.CODE,
|
||||
onChange: (_mForm, v: HookCodeType, { setModel }) => {
|
||||
if (v === HookCodeType.DATA_SOURCE_METHOD) {
|
||||
setModel('codeId', []);
|
||||
} else {
|
||||
setModel('codeId', '');
|
||||
}
|
||||
|
||||
display: (_mForm, { model }) => model.codeType !== HookCodeType.DATA_SOURCE_METHOD,
|
||||
notEditable: () => !codeBlockService.getEditStatus(),
|
||||
},
|
||||
{
|
||||
type: 'data-source-method-select',
|
||||
name: 'codeId',
|
||||
text: '数据源字段',
|
||||
rules: [{ typeMatch: true, trigger: 'change' }],
|
||||
display: (_mForm, { model }) => model.codeType === HookCodeType.DATA_SOURCE_METHOD,
|
||||
notEditable: () => !dataSourceService.get('editable'),
|
||||
return v;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'code-select-col',
|
||||
name: 'codeId',
|
||||
span: 18,
|
||||
labelWidth: 0,
|
||||
display: (_mForm, { model }) => model.codeType !== HookCodeType.DATA_SOURCE_METHOD,
|
||||
notEditable: () => !codeBlockService.getEditStatus(),
|
||||
rules: [{ typeMatch: true, trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
type: 'data-source-method-select',
|
||||
name: 'codeId',
|
||||
span: 18,
|
||||
labelWidth: 0,
|
||||
display: (_mForm, { model }) => model.codeType === HookCodeType.DATA_SOURCE_METHOD,
|
||||
notEditable: () => !dataSourceService.get('editable'),
|
||||
rules: [{ typeMatch: true, trigger: 'change' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
@ -147,7 +147,6 @@ const selectConfig: SelectConfig = {
|
||||
type: 'select',
|
||||
name: props.name,
|
||||
disabled: props.disabled,
|
||||
text: props.config.text,
|
||||
options: () => {
|
||||
if (codeDsl.value) {
|
||||
return map(codeDsl.value, (value, key) => ({
|
||||
|
||||
@ -1,12 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="m-fields-data-source-field-select"
|
||||
:class="{
|
||||
[`data-source-field-${type}`]: !isSelectValid,
|
||||
}"
|
||||
>
|
||||
<div class="m-fields-data-source-field-select">
|
||||
<FieldSelect
|
||||
v-if="isSelectValid"
|
||||
v-if="!disabledDataSource && (showDataSourceFieldSelect || !config.fieldConfig)"
|
||||
:model-value="model[name]"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
@ -42,14 +37,15 @@
|
||||
:size="size"
|
||||
:disabled="disabled"
|
||||
@click="onToggleDataSourceFieldSelectHandler"
|
||||
><MIcon :icon="dataSourceIcon"></MIcon>
|
||||
</TMagicButton>
|
||||
><MIcon :icon="Coin"></MIcon
|
||||
></TMagicButton>
|
||||
</TMagicTooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, resolveComponent, watch } from 'vue';
|
||||
import { Coin } from '@element-plus/icons-vue';
|
||||
|
||||
import { DataSchema } from '@tmagic/core';
|
||||
import { TMagicButton, tMagicMessage, TMagicTooltip } from '@tmagic/design';
|
||||
@ -65,8 +61,6 @@ import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, removeDataSourceFieldPrefix } f
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
|
||||
import dataSourceIcon from '../../icons/DatasourceIcon.vue';
|
||||
|
||||
import FieldSelect from './FieldSelect.vue';
|
||||
|
||||
defineOptions({
|
||||
@ -81,9 +75,6 @@ const props = withDefaults(defineProps<FieldProps<DataSourceFieldSelectConfig>>(
|
||||
|
||||
const showDataSourceFieldSelect = ref(false);
|
||||
|
||||
const isSelectValid = computed(
|
||||
() => !disabledDataSource.value && (showDataSourceFieldSelect.value || !props.config.fieldConfig),
|
||||
);
|
||||
watch(
|
||||
() => props.model[props.name],
|
||||
(value) => {
|
||||
|
||||
@ -68,15 +68,12 @@ const config = computed<GroupListConfig>(() => ({
|
||||
titlePrefix: props.config.titlePrefix,
|
||||
expandAll: true,
|
||||
enableToggleMode: false,
|
||||
flat: props.config.flat,
|
||||
items: [
|
||||
{
|
||||
type: 'table',
|
||||
name: 'cond',
|
||||
operateColWidth: props.config.operateColWidth,
|
||||
operateColWidth: 80,
|
||||
enableToggleMode: false,
|
||||
fixed: props.config.fixed,
|
||||
flat: props.config.flat,
|
||||
items: [
|
||||
parentFields.value.length
|
||||
? {
|
||||
|
||||
@ -12,21 +12,16 @@
|
||||
@change="onChangeHandler"
|
||||
></MTable>
|
||||
|
||||
<div v-else class="fullWidth event-select-container">
|
||||
<div class="event-select-header">
|
||||
<div class="event-select-title">事件配置</div>
|
||||
<TMagicButton
|
||||
v-if="!isCompareMode && displayList.length > 0"
|
||||
class="create-button"
|
||||
text
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
:size="size"
|
||||
:disabled="disabled"
|
||||
@click="addEvent()"
|
||||
>添加事件</TMagicButton
|
||||
>
|
||||
</div>
|
||||
<div v-else class="fullWidth">
|
||||
<TMagicButton
|
||||
v-if="!isCompareMode"
|
||||
class="create-button"
|
||||
type="primary"
|
||||
:size="size"
|
||||
:disabled="disabled"
|
||||
@click="addEvent()"
|
||||
>添加事件</TMagicButton
|
||||
>
|
||||
<MPanel
|
||||
v-for="entry in displayList"
|
||||
:key="entry.index"
|
||||
@ -37,45 +32,32 @@
|
||||
:model="entry.cardItem"
|
||||
:last-values="entry.lastCardItem"
|
||||
:is-compare="isCompareMode"
|
||||
:hide-expand="true"
|
||||
:label-width="config.labelWidth || '100px'"
|
||||
@change="onChangeHandler"
|
||||
>
|
||||
<template #header>
|
||||
<div class="event-item-header">
|
||||
<div class="event-item-title">事件{{ Number(entry.index) + 1 }}</div>
|
||||
<MFormContainer
|
||||
class="fullWidth"
|
||||
:config="eventNameConfig"
|
||||
:model="entry.cardItem"
|
||||
:last-values="entry.lastCardItem"
|
||||
:is-compare="isCompareMode"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
:prop="`${prop}.${entry.index}`"
|
||||
@change="eventNameChangeHandler"
|
||||
></MFormContainer>
|
||||
<TMagicButton
|
||||
class="event-item-delete-button"
|
||||
v-if="!isCompareMode"
|
||||
link
|
||||
:icon="Delete"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
@click="removeEvent(Number(entry.index))"
|
||||
></TMagicButton>
|
||||
</div>
|
||||
<MFormContainer
|
||||
class="fullWidth"
|
||||
:config="eventNameConfig"
|
||||
:model="entry.cardItem"
|
||||
:last-values="entry.lastCardItem"
|
||||
:is-compare="isCompareMode"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
:prop="`${prop}.${entry.index}`"
|
||||
@change="eventNameChangeHandler"
|
||||
></MFormContainer>
|
||||
<TMagicButton
|
||||
v-if="!isCompareMode"
|
||||
style="color: #f56c6c"
|
||||
link
|
||||
:icon="Delete"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
@click="removeEvent(Number(entry.index))"
|
||||
></TMagicButton>
|
||||
</template>
|
||||
</MPanel>
|
||||
|
||||
<TMagicButton
|
||||
v-if="!isCompareMode"
|
||||
class="create-button fullWidth"
|
||||
:icon="Plus"
|
||||
:disabled="disabled"
|
||||
@click="addEvent()"
|
||||
>添加事件</TMagicButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -83,7 +65,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { Delete } from '@element-plus/icons-vue';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { has } from 'lodash-es';
|
||||
|
||||
import { ActionType } from '@tmagic/core';
|
||||
@ -127,7 +108,7 @@ const { editorService, dataSourceService, eventsService, codeBlockService, props
|
||||
const eventNameConfig = computed(() => {
|
||||
const defaultEventNameConfig = {
|
||||
name: 'name',
|
||||
text: '事件类型',
|
||||
text: '事件',
|
||||
type: (mForm: FormState | undefined, { formValue }: any) => {
|
||||
if (
|
||||
props.config.src !== 'component' ||
|
||||
@ -137,7 +118,7 @@ const eventNameConfig = computed(() => {
|
||||
}
|
||||
return 'select';
|
||||
},
|
||||
labelWidth: '70px',
|
||||
labelWidth: '40px',
|
||||
checkStrictly: () => props.config.src !== 'component',
|
||||
valueSeparator: '.',
|
||||
options: (_mForm: FormState, { formValue }: any) => getEventNameOptions(props.config.src, formValue),
|
||||
@ -197,7 +178,6 @@ const actionTypeConfig = computed(() => {
|
||||
name: 'actionType',
|
||||
text: '联动类型',
|
||||
type: 'select',
|
||||
labelPosition: 'left',
|
||||
defaultValue: ActionType.COMP,
|
||||
options: actionTypeOptions.value,
|
||||
rules: [
|
||||
@ -210,12 +190,6 @@ const actionTypeConfig = computed(() => {
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
onChange: (_mForm: FormState, _v: string, { setModel }: any) => {
|
||||
setModel('to', '');
|
||||
setModel('method', '');
|
||||
setModel('codeId', '');
|
||||
setModel('dataSourceMethod', []);
|
||||
},
|
||||
};
|
||||
return { ...defaultActionTypeConfig, ...props.config.actionTypeConfig };
|
||||
});
|
||||
@ -226,9 +200,8 @@ const targetCompConfig = computed(() => {
|
||||
name: 'to',
|
||||
text: '联动组件',
|
||||
type: 'ui-select',
|
||||
labelPosition: 'left',
|
||||
display: (_mForm, { model }) => model.actionType === ActionType.COMP,
|
||||
onChange: (_mForm, _v, { setModel }) => {
|
||||
onChange: (_MForm, _v, { setModel }) => {
|
||||
setModel('method', '');
|
||||
},
|
||||
rules: [
|
||||
@ -246,7 +219,6 @@ const compActionConfig = computed(() => {
|
||||
const defaultCompActionConfig: DynamicTypeConfig = {
|
||||
name: 'method',
|
||||
text: '动作',
|
||||
labelPosition: 'left',
|
||||
type: (mForm: FormState | undefined, { model }: any) => {
|
||||
const to = editorService.getNodeById(model.to);
|
||||
|
||||
@ -345,7 +317,6 @@ const actionsConfig = computed(
|
||||
() =>
|
||||
defineFormItem({
|
||||
type: 'panel',
|
||||
labelPosition: 'left',
|
||||
items: [
|
||||
{
|
||||
type: 'group-list',
|
||||
@ -353,7 +324,6 @@ const actionsConfig = computed(
|
||||
expandAll: true,
|
||||
enableToggleMode: false,
|
||||
titlePrefix: '动作',
|
||||
labelPosition: 'left',
|
||||
items: [
|
||||
actionTypeConfig.value,
|
||||
targetCompConfig.value,
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<TMagicCollapse class="m-fields-style-setter" v-model="collapseValue">
|
||||
<template v-for="(item, index) in list" :key="index">
|
||||
<TMagicCollapseItem :name="`${index}`">
|
||||
<template #title>{{ item.title }}</template>
|
||||
<template #title><MIcon :icon="Grid"></MIcon>{{ item.title }}</template>
|
||||
<component
|
||||
v-if="item.component"
|
||||
:is="item.component"
|
||||
@ -22,11 +22,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { shallowRef } from 'vue';
|
||||
import { Grid } from '@element-plus/icons-vue';
|
||||
|
||||
import { TMagicCollapse, TMagicCollapseItem } from '@tmagic/design';
|
||||
import type { ContainerChangeEventData, FieldProps } from '@tmagic/form';
|
||||
import type { StyleSchema } from '@tmagic/schema';
|
||||
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
|
||||
import { Background, Border, Font, Layout, Position, Transform } from './pro/';
|
||||
|
||||
defineOptions({
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
@click="selectDirection('Left')"
|
||||
></div>
|
||||
<div
|
||||
class="border-icon center"
|
||||
class="border-icon"
|
||||
:class="{ active: direction === '', configured: isConfigured('') }"
|
||||
@click="selectDirection()"
|
||||
></div>
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M12 4C12.5523 4 13 4.44772 13 5V11C13 11.5523 12.5523 12 12 12H4C3.44772 12 3 11.5523 3 11V5C3 4.44772 3.44772 4 4 4H12ZM4.5 5C4.22386 5 4 5.22386 4 5.5V10.5C4 10.7761 4.22386 11 4.5 11H11.5C11.7761 11 12 10.7761 12 10.5V5.5C12 5.22386 11.7761 5 11.5 5H4.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="13.75"
|
||||
y="7.75"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
transform="rotate(90 13.75 7.75)"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M12 3C12.5523 3 13 3.44772 13 4V10C13 10.5523 12.5523 11 12 11H4C3.44772 11 3 10.5523 3 10V4C3 3.44772 3.44772 3 4 3H12ZM4.5 4C4.22386 4 4 4.22386 4 4.5V9.5C4 9.77614 4.22386 10 4.5 10H11.5C11.7761 10 12 9.77614 12 9.5V4.5C12 4.22386 11.7761 4 11.5 4H4.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="13.75"
|
||||
y="12.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
transform="rotate(90 13.75 12.25)"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -1,19 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M12 5C12.5523 5 13 5.44772 13 6V12C13 12.5523 12.5523 13 12 13H4C3.44772 13 3 12.5523 3 12V6C3 5.44772 3.44772 5 4 5H12ZM4.5 6C4.22386 6 4 6.22386 4 6.5V11.5C4 11.7761 4.22386 12 4.5 12H11.5C11.7761 12 12 11.7761 12 11.5V6.5C12 6.22386 11.7761 6 11.5 6H4.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="13.75"
|
||||
y="3.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
transform="rotate(90 13.75 3.25)"
|
||||
stroke="white"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: white; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M12 8C12.5523 8 13 8.44772 13 9V12C13 12.5523 12.5523 13 12 13H4C3.44772 13 3 12.5523 3 12V9C3 8.44772 3.44772 8 4 8H12ZM4.5 9C4.22386 9 4 9.22386 4 9.5V11.5C4 11.7761 4.22386 12 4.5 12H11.5C11.7761 12 12 11.7761 12 11.5V9.5C12 9.22386 11.7761 9 11.5 9H4.5Z"
|
||||
/>
|
||||
<path
|
||||
d="M12 2C12.5523 2 13 2.44772 13 3V6C13 6.55228 12.5523 7 12 7H4C3.44772 7 3 6.55228 3 6V3C3 2.44772 3.44772 2 4 2H12ZM4.5 3C4.22386 3 4 3.22386 4 3.5V5.5C4 5.77614 4.22386 6 4.5 6H11.5C11.7761 6 12 5.77614 12 5.5V3.5C12 3.22386 11.7761 3 11.5 3H4.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="13.75"
|
||||
y="10.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
transform="rotate(90 13.75 10.25)"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<rect
|
||||
x="13.75"
|
||||
y="4.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
transform="rotate(90 13.75 4.25)"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M12 9C12.5523 9 13 9.44772 13 10V12C13 12.5523 12.5523 13 12 13H4C3.44772 13 3 12.5523 3 12V10C3 9.44772 3.44772 9 4 9H12ZM4.5 10C4.22386 10 4 10.2239 4 10.5V11.5C4 11.7761 4.22386 12 4.5 12H11.5C11.7761 12 12 11.7761 12 11.5V10.5C12 10.2239 11.7761 10 11.5 10H4.5Z"
|
||||
/>
|
||||
<path
|
||||
d="M12 3C12.5523 3 13 3.44772 13 4V6C13 6.55228 12.5523 7 12 7H4C3.44772 7 3 6.55228 3 6V4C3 3.44772 3.44772 3 4 3H12ZM4.5 4C4.22386 4 4 4.22386 4 4.5V5.5C4 5.77614 4.22386 6 4.5 6H11.5C11.7761 6 12 5.77614 12 5.5V4.5C12 4.22386 11.7761 4 11.5 4H4.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="13.75"
|
||||
y="14.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
transform="rotate(90 13.75 14.25)"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<rect
|
||||
x="13.75"
|
||||
y="1.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
transform="rotate(90 13.75 1.25)"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -1,5 +0,0 @@
|
||||
export { default as AlignItemsCenter } from './Center.vue';
|
||||
export { default as AlignItemsFlexEnd } from './FlexEnd.vue';
|
||||
export { default as AlignItemsFlexStart } from './FlexStart.vue';
|
||||
export { default as AlignItemsSpaceAround } from './SpaceAround.vue';
|
||||
export { default as AlignItemsSpaceBetween } from './SpaceBetween.vue';
|
||||
@ -1,24 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M0 0.625C0 0.279822 0.248731 0 0.555556 0H9.44444C9.75127 0 10 0.279822 10 0.625C10 0.970178 9.75127 1.25 9.44444 1.25H0.555556C0.248731 1.25 0 0.970178 0 0.625Z"
|
||||
fill-opacity="0.3"
|
||||
/>
|
||||
<path
|
||||
d="M0 9.375C0 9.02982 0.248731 8.75 0.555556 8.75H9.44444C9.75127 8.75 10 9.02982 10 9.375C10 9.72018 9.75127 10 9.44444 10H0.555556C0.248731 10 0 9.72018 0 9.375Z"
|
||||
fill-opacity="0.3"
|
||||
/>
|
||||
<path
|
||||
d="M0.555556 4.375C0.248731 4.375 0 4.65482 0 5C0 5.34518 0.248731 5.625 0.555556 5.625H2.87307L3.42868 4.99994L2.87318 4.375H0.555556Z"
|
||||
fill-opacity="0.3"
|
||||
/>
|
||||
<path
|
||||
d="M9.44444 5.625H7.12699L6.57138 4.99994L7.12688 4.375H9.44444C9.75127 4.375 10 4.65482 10 5C10 5.34518 9.75127 5.625 9.44444 5.625Z"
|
||||
fill-opacity="0.3"
|
||||
/>
|
||||
<path
|
||||
transform="translate(0 2.6072)"
|
||||
d="M3.30542 0.183058C3.52237 -0.0610194 3.87413 -0.0610194 4.09109 0.183058C4.30805 0.427136 4.30805 0.822864 4.09109 1.06694L2.91256 2.39279L4.0911 3.71865C4.30806 3.96273 4.30806 4.35846 4.0911 4.60253C3.87414 4.84661 3.52239 4.84661 3.30543 4.60253L2.12688 3.27667L0.948393 4.60248C0.731435 4.84655 0.379676 4.84655 0.162719 4.60248C-0.0542395 4.3584 -0.0542395 3.96267 0.162719 3.71859L1.34121 2.39279L0.162731 1.067C-0.0542269 0.822922 -0.0542268 0.427194 0.162731 0.183116C0.379689 -0.0609612 0.731447 -0.0609612 0.948405 0.183116L2.12688 1.50891L3.30542 0.183058Z"
|
||||
/>
|
||||
d="M912.526651 867.741144 555.540144 510.712681l356.986507-357.000833c11.171434-11.18576 11.171434-29.257348 0-40.443108-11.20111-11.18576-29.272697-11.18576-40.444131 0L515.096013 470.267527 158.096203 113.267716c-11.187807-11.159154-29.258371-11.159154-40.444131 0-11.186783 11.186783-11.186783 29.286 0 40.47176L474.623229 510.712681 117.623419 867.741144c-11.159154 11.172457-11.159154 29.216415 0 40.443108 11.18576 11.17348 29.284977 11.17348 40.47176 0l357.000833-357.027439 356.985484 357.027439c11.171434 11.17348 29.243021 11.17348 40.444131 0C923.698085 896.957559 923.725714 878.913601 912.526651 867.741144z"
|
||||
fill="#5D5D5D"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,13 +1,31 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<rect x="13" y="3" width="2" height="2" rx="0.5" transform="rotate(90 13 3)" fill-opacity="0.3" />
|
||||
<rect x="13" y="7" width="2" height="2" rx="0.5" transform="rotate(90 13 7)" fill-opacity="0.3" />
|
||||
<rect x="13" y="11" width="2" height="2" rx="0.5" transform="rotate(90 13 11)" fill-opacity="0.3" />
|
||||
<rect x="9" y="3" width="2" height="2" rx="0.5" transform="rotate(90 9 3)" />
|
||||
<rect x="9" y="7" width="2" height="2" rx="0.5" transform="rotate(90 9 7)" />
|
||||
<rect x="9" y="11" width="2" height="2" rx="0.5" transform="rotate(90 9 11)" fill-opacity="0.3" />
|
||||
<rect x="5" y="3" width="2" height="2" rx="0.5" transform="rotate(90 5 3)" />
|
||||
<rect x="5" y="7" width="2" height="2" rx="0.5" transform="rotate(90 5 7)" />
|
||||
<rect x="5" y="11" width="2" height="2" rx="0.5" transform="rotate(90 5 11)" fill-opacity="0.3" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M884.736 102.4l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456C696.32 311.296 712.704 327.68 733.184 327.68l147.456 0c20.48 0 36.864-16.384 36.864-36.864L917.504 139.264C921.6 118.784 905.216 102.4 884.736 102.4zM884.736 290.816l-147.456 0L737.28 139.264l147.456 0L884.736 290.816z"
|
||||
></path>
|
||||
<path
|
||||
d="M884.736 696.32l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C921.6 712.704 905.216 696.32 884.736 696.32zM884.736 884.736l-147.456 0 0-147.456 147.456 0L884.736 884.736z"
|
||||
></path>
|
||||
<path
|
||||
d="M884.736 401.408l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C921.6 417.792 905.216 401.408 884.736 401.408zM884.736 585.728l-147.456 0 0-147.456 147.456 0L884.736 585.728z"
|
||||
></path>
|
||||
<path
|
||||
d="M585.728 401.408l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C622.592 417.792 606.208 401.408 585.728 401.408zM585.728 585.728l-147.456 0 0-147.456 147.456 0L585.728 585.728z"
|
||||
></path>
|
||||
<path
|
||||
d="M585.728 102.4l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864L622.592 139.264C622.592 118.784 606.208 102.4 585.728 102.4zM585.728 290.816l-147.456 0L438.272 139.264l147.456 0L585.728 290.816z"
|
||||
></path>
|
||||
<path
|
||||
d="M585.728 696.32l-147.456 0c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C622.592 712.704 606.208 696.32 585.728 696.32zM585.728 884.736l-147.456 0 0-147.456 147.456 0L585.728 884.736z"
|
||||
></path>
|
||||
<path
|
||||
d="M290.816 696.32 139.264 696.32c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C327.68 712.704 311.296 696.32 290.816 696.32zM290.816 884.736 139.264 884.736l0-147.456 147.456 0L286.72 884.736z"
|
||||
></path>
|
||||
<path
|
||||
d="M290.816 401.408 139.264 401.408c-20.48 0-36.864 16.384-36.864 36.864l0 147.456c0 20.48 16.384 36.864 36.864 36.864l147.456 0c20.48 0 36.864-16.384 36.864-36.864l0-147.456C327.68 417.792 311.296 401.408 290.816 401.408zM290.816 585.728 139.264 585.728l0-147.456 147.456 0L286.72 585.728z"
|
||||
></path>
|
||||
<path
|
||||
d="M290.816 102.4 139.264 102.4c-20.48 0-36.864 16.384-36.864 36.864l0 147.456C102.4 311.296 118.784 327.68 139.264 327.68l147.456 0C311.296 327.68 327.68 311.296 327.68 290.816L327.68 139.264C327.68 118.784 311.296 102.4 290.816 102.4zM290.816 290.816 139.264 290.816 139.264 139.264l147.456 0L286.72 290.816z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<rect x="3" y="3" width="2" height="10" rx="1" />
|
||||
<rect x="7" y="3" width="2" height="10" rx="1" />
|
||||
<rect x="11" y="3" width="2" height="10" rx="1" fill-opacity="0.3" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M796.444444 170.666667h-227.555555v682.666666h227.555555V170.666667z m-56.888888 625.777777h-113.777778V227.555556h113.777778v568.888888zM455.111111 170.666667H227.555556v682.666666h227.555555V170.666667zM398.222222 796.444444H284.444444V227.555556h113.777778v568.888888zM910.222222 56.888889h56.888889v910.222222h-56.888889zM56.888889 56.888889h56.888889v910.222222H56.888889z"
|
||||
fill="#333333"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<rect x="13" y="3" width="2" height="10" rx="1" transform="rotate(90 13 3)" />
|
||||
<rect x="13" y="7" width="2" height="10" rx="1" transform="rotate(90 13 7)" />
|
||||
<rect x="13" y="11" width="2" height="10" rx="1" transform="rotate(90 13 11)" fill-opacity="0.3" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M170.666667 227.555556v227.555555h682.666666V227.555556H170.666667z m625.777777 56.888888v113.777778H227.555556V284.444444h568.888888zM170.666667 568.888889v227.555555h682.666666v-227.555555H170.666667z m625.777777 56.888889v113.777778H227.555556v-113.777778h568.888888zM56.888889 56.888889h910.222222v56.888889H56.888889zM56.888889 910.222222h910.222222v56.888889H56.888889z"
|
||||
fill="#333333"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M12 3C12.5523 3 13 3.44772 13 4V10C13 10.5523 12.5523 11 12 11H4C3.44772 11 3 10.5523 3 10V4C3 3.44772 3.44772 3 4 3H12ZM4.5 4C4.22386 4 4 4.22386 4 4.5V9.5C4 9.77614 4.22386 10 4.5 10H11.5C11.7761 10 12 9.77614 12 9.5V4.5C12 4.22386 11.7761 4 11.5 4H4.5Z"
|
||||
/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 12L8 15L5 12L11 12Z" fill-opacity="0.3" />
|
||||
d="M896 320H128V0h768v320z m0 320H128v-256h768v256z m-128 192l-256 192-256-192 192-0.032V704h128v128h192z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M12 5C12.5523 5 13 5.44772 13 6V12C13 12.5523 12.5523 13 12 13H4C3.44772 13 3 12.5523 3 12V6C3 5.44772 3.44772 5 4 5H12ZM4.5 6C4.22386 6 4 6.22386 4 6.5V11.5C4 11.7761 4.22386 12 4.5 12H11.5C11.7761 12 12 11.7761 12 11.5V6.5C12 6.22386 11.7761 6 11.5 6H4.5Z"
|
||||
/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 4L8 1L11 4L5 4Z" fill-opacity="0.3" />
|
||||
d="M896 704H128v320h768V704z m0-320H128v256h768v-256z m-128-192l-256-192-256 192 192 0.032V320h128V192h192z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M10 4C10.5523 4 11 4.44772 11 5V11C11 11.5523 10.5523 12 10 12H2C1.44772 12 1 11.5523 1 11V5C1 4.44772 1.44772 4 2 4H10ZM2.5 5C2.22386 5 2 5.22386 2 5.5V10.5C2 10.7761 2.22386 11 2.5 11H9.5C9.77614 11 10 10.7761 10 10.5V5.5C10 5.22386 9.77614 5 9.5 5H2.5Z"
|
||||
/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 5L15 8L12 11L12 5Z" fill-opacity="0.3" />
|
||||
d="M320 128v768H0V128h320z m320 0v768h-256V128h256z m192 128l192 256-192 256-0.032-192H704v-128h128V256z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M14 4C14.5523 4 15 4.44772 15 5V11C15 11.5523 14.5523 12 14 12H6C5.44772 12 5 11.5523 5 11V5C5 4.44772 5.44772 4 6 4H14ZM6.5 5C6.22386 5 6 5.22386 6 5.5V10.5C6 10.7761 6.22386 11 6.5 11H13.5C13.7761 11 14 10.7761 14 10.5V5.5C14 5.22386 13.7761 5 13.5 5H6.5Z"
|
||||
/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 11L1 8L4 5L4 11Z" fill-opacity="0.3" />
|
||||
d="M704 128v768h320V128H704zM384 128v768h256V128h-256zM192 256l-192 256 192 256 0.032-192H320v-128H192V256z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M13 4C13.5523 4 14 4.44772 14 5V11C14 11.5523 13.5523 12 13 12H3C2.44772 12 2 11.5523 2 11V5C2 4.44772 2.44772 4 3 4H13ZM3.59082 5C3.31472 5.00005 3.09082 5.22389 3.09082 5.5V10.5C3.09082 10.7761 3.31472 11 3.59082 11H12.4092C12.6853 11 12.9092 10.7761 12.9092 10.5V5.5C12.9092 5.22389 12.6853 5.00005 12.4092 5H3.59082Z"
|
||||
/>
|
||||
<rect x="7.5" y="2" width="1" height="12" rx="0.5" fill-opacity="0.3" style="fill: black; fill-opacity: 0.3" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path d="M480 1024V0h64v1024h-64z m128-64V64h320v896H608zM96 960V64h320v896H96z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,18 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M11 4C11.5523 4 12 4.44772 12 5V11C12 11.5523 11.5523 12 11 12H2C1.44772 12 1 11.5523 1 11V5C1 4.44772 1.44772 4 2 4H11ZM2.5 5C2.22386 5 2 5.22386 2 5.5V10.5C2 10.7761 2.22386 11 2.5 11H10.5C10.7761 11 11 10.7761 11 10.5V5.5C11 5.22386 10.7761 5 10.5 5H2.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="14.25"
|
||||
y="2.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path d="M416 160H96v704h320V160z m384 0H480v704h320V160z m128-160h-64v1024h64V0z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,18 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M14 4C14.5523 4 15 4.44772 15 5V11C15 11.5523 14.5523 12 14 12H5C4.44772 12 4 11.5523 4 11V5C4 4.44772 4.44772 4 5 4H14ZM5.5 5C5.22386 5 5 5.22386 5 5.5V10.5C5 10.7761 5.22386 11 5.5 11H13.5C13.7761 11 14 10.7761 14 10.5V5.5C14 5.22386 13.7761 5 13.5 5H5.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="1.25"
|
||||
y="2.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
stroke="white"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: white; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path d="M608 160h320v704H608V160zM224 160h320v704H224V160zM96 0h64v1024H96V0z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,32 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path
|
||||
d="M6 4C6.55228 4 7 4.44772 7 5V11C7 11.5523 6.55228 12 6 12H3C2.44772 12 2 11.5523 2 11V5C2 4.44772 2.44772 4 3 4H6ZM3.5 5C3.22386 5 3 5.22386 3 5.5V10.5C3 10.7761 3.22386 11 3.5 11H5.5C5.77614 11 6 10.7761 6 10.5V5.5C6 5.22386 5.77614 5 5.5 5H3.5Z"
|
||||
/>
|
||||
<path
|
||||
d="M13 4C13.5523 4 14 4.44772 14 5V11C14 11.5523 13.5523 12 13 12H10C9.44772 12 9 11.5523 9 11V5C9 4.44772 9.44772 4 10 4H13ZM10.5 5C10.2239 5 10 5.22386 10 5.5V10.5C10 10.7761 10.2239 11 10.5 11H12.5C12.7761 11 13 10.7761 13 10.5V5.5C13 5.22386 12.7761 5 12.5 5H10.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="11.25"
|
||||
y="2.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<rect
|
||||
x="4.25"
|
||||
y="2.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
d="M320 864H192v160H128v-160H0V160h128V0h64v160h128v704z m704 0h-128v160h-64v-160h-128V160h128V0h64v160h128v704z"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -1,31 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path
|
||||
d="M6 4C6.55228 4 7 4.44772 7 5V11C7 11.5523 6.55228 12 6 12H4C3.44772 12 3 11.5523 3 11V5C3 4.44772 3.44772 4 4 4H6ZM4.5 5C4.22386 5 4 5.22386 4 5.5V10.5C4 10.7761 4.22386 11 4.5 11H5.5C5.77614 11 6 10.7761 6 10.5V5.5C6 5.22386 5.77614 5 5.5 5H4.5Z"
|
||||
/>
|
||||
<path
|
||||
d="M12 4C12.5523 4 13 4.44772 13 5V11C13 11.5523 12.5523 12 12 12H10C9.44772 12 9 11.5523 9 11V5C9 4.44772 9.44772 4 10 4H12ZM10.5 5C10.2239 5 10 5.22386 10 5.5V10.5C10 10.7761 10.2239 11 10.5 11H11.5C11.7761 11 12 10.7761 12 10.5V5.5C12 5.22386 11.7761 5 11.5 5H10.5Z"
|
||||
/>
|
||||
<rect
|
||||
x="14.25"
|
||||
y="2.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<rect
|
||||
x="1.25"
|
||||
y="2.25"
|
||||
width="0.5"
|
||||
height="11.5"
|
||||
rx="0.25"
|
||||
stroke="black"
|
||||
stroke-opacity="0.3"
|
||||
style="stroke: black; stroke-opacity: 0.3"
|
||||
stroke-width="0.5"
|
||||
/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<path d="M64 1024H0V0h64v1024z m384-160H128V160h320v704z m448 0H576V160h320v704z m128 160h-64V0h64v1024z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@ -29,7 +29,6 @@ defineProps<{
|
||||
isCompare?: boolean;
|
||||
disabled?: boolean;
|
||||
size?: 'large' | 'default' | 'small';
|
||||
theme?: string;
|
||||
prop?: string;
|
||||
}>();
|
||||
|
||||
@ -113,14 +112,10 @@ const formConfig = defineFormConfig([
|
||||
childType: 'button',
|
||||
labelWidth: '68px',
|
||||
options: [
|
||||
{ value: 'no-repeat', icon: markRaw(BackgroundNoRepeat), tooltip: '不重复 no-repeat' },
|
||||
{ value: 'repeat', icon: markRaw(BackgroundRepeat), tooltip: '垂直和水平方向重复 repeat' },
|
||||
{ value: 'repeat-x', icon: markRaw(BackgroundRepeatX), tooltip: '水平方向重复 repeat-x' },
|
||||
{ value: 'repeat-y', icon: markRaw(BackgroundRepeatY), tooltip: '垂直方向重复 repeat-y' },
|
||||
{
|
||||
value: 'repeat',
|
||||
icon: markRaw(BackgroundRepeat),
|
||||
tooltip: '垂直和水平方向重复 repeat',
|
||||
},
|
||||
{ value: 'no-repeat', icon: markRaw(BackgroundNoRepeat), tooltip: '不重复 no-repeat' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -95,9 +95,9 @@ const formConfig = defineFormConfig([
|
||||
childType: 'button',
|
||||
labelWidth: '68px',
|
||||
options: [
|
||||
{ value: 'left', icon: markRaw(AlignLeft), tooltip: '左对齐 row', text: '左对齐' },
|
||||
{ value: 'center', icon: markRaw(AlignCenter), tooltip: '居中对齐 center', text: '居中对齐' },
|
||||
{ value: 'right', icon: markRaw(AlignRight), tooltip: '右对齐 right', text: '右对齐' },
|
||||
{ value: 'left', icon: markRaw(AlignLeft), tooltip: '左对齐 row' },
|
||||
{ value: 'center', icon: markRaw(AlignCenter), tooltip: '居中对齐 center' },
|
||||
{ value: 'right', icon: markRaw(AlignRight), tooltip: '右对齐 right' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@ -26,19 +26,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
import { useTheme } from '@tmagic/design';
|
||||
import type { ContainerChangeEventData } from '@tmagic/form';
|
||||
import { defineFormConfig, MContainer } from '@tmagic/form';
|
||||
import type { StyleSchema } from '@tmagic/schema';
|
||||
|
||||
import Box from '../components/Box.vue';
|
||||
import {
|
||||
AlignItemsCenter,
|
||||
AlignItemsFlexEnd,
|
||||
AlignItemsFlexStart,
|
||||
AlignItemsSpaceAround,
|
||||
AlignItemsSpaceBetween,
|
||||
} from '../icons/align-items';
|
||||
import { DisplayBlock, DisplayFlex, DisplayInline, DisplayInlineBlock, DisplayNone } from '../icons/display';
|
||||
import {
|
||||
FlexDirectionColumn,
|
||||
@ -54,13 +46,12 @@ import {
|
||||
JustifyContentSpaceBetween,
|
||||
} from '../icons/justify-content';
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
values: Partial<StyleSchema>;
|
||||
lastValues?: Partial<StyleSchema>;
|
||||
isCompare?: boolean;
|
||||
disabled?: boolean;
|
||||
size?: 'large' | 'default' | 'small';
|
||||
theme?: string;
|
||||
prop?: string;
|
||||
}>();
|
||||
|
||||
@ -69,42 +60,19 @@ const emit = defineEmits<{
|
||||
addDiffCount: [];
|
||||
}>();
|
||||
|
||||
const displayTheme = useTheme(props);
|
||||
|
||||
const formConfig = defineFormConfig([
|
||||
{
|
||||
name: 'display',
|
||||
text: '模式',
|
||||
type: 'radioGroup',
|
||||
childType: 'button',
|
||||
labelWidth: '90px',
|
||||
iconSize: '24px',
|
||||
labelWidth: '68px',
|
||||
options: [
|
||||
{
|
||||
value: 'inline',
|
||||
icon: markRaw(DisplayInline),
|
||||
tooltip: '内联布局 inline',
|
||||
},
|
||||
{
|
||||
value: 'flex',
|
||||
icon: markRaw(DisplayFlex),
|
||||
tooltip: '弹性布局 flex',
|
||||
},
|
||||
{
|
||||
value: 'block',
|
||||
icon: markRaw(DisplayBlock),
|
||||
tooltip: '块级布局 block',
|
||||
},
|
||||
{
|
||||
value: 'inline-block',
|
||||
icon: markRaw(DisplayInlineBlock),
|
||||
tooltip: '内联块布局 inline-block',
|
||||
},
|
||||
{
|
||||
value: 'none',
|
||||
icon: markRaw(DisplayNone),
|
||||
tooltip: '隐藏 none',
|
||||
},
|
||||
{ value: 'inline', icon: markRaw(DisplayInline), tooltip: '内联布局 inline' },
|
||||
{ value: 'flex', icon: markRaw(DisplayFlex), tooltip: '弹性布局 flex' },
|
||||
{ value: 'block', icon: markRaw(DisplayBlock), tooltip: '块级布局 block' },
|
||||
{ value: 'inline-block', icon: markRaw(DisplayInlineBlock), tooltip: '内联块布局 inline-block' },
|
||||
{ value: 'none', icon: markRaw(DisplayNone), tooltip: '隐藏 none' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -112,20 +80,11 @@ const formConfig = defineFormConfig([
|
||||
text: '主轴方向',
|
||||
type: 'radioGroup',
|
||||
childType: 'button',
|
||||
labelWidth: '90px',
|
||||
iconSize: '24px',
|
||||
labelWidth: '68px',
|
||||
options: [
|
||||
{ value: 'row', icon: markRaw(FlexDirectionRow), tooltip: '水平方向 起点在左侧 row' },
|
||||
{
|
||||
value: 'row-reverse',
|
||||
icon: markRaw(FlexDirectionRowReverse),
|
||||
tooltip: '水平方向 起点在右侧 row-reverse',
|
||||
},
|
||||
{
|
||||
value: 'column',
|
||||
icon: markRaw(FlexDirectionColumn),
|
||||
tooltip: '垂直方向 起点在上沿 column',
|
||||
},
|
||||
{ value: 'row-reverse', icon: markRaw(FlexDirectionRowReverse), tooltip: '水平方向 起点在右侧 row-reverse' },
|
||||
{ value: 'column', icon: markRaw(FlexDirectionColumn), tooltip: '垂直方向 起点在上沿 column' },
|
||||
{
|
||||
value: 'column-reverse',
|
||||
icon: markRaw(FlexDirectionColumnReverse),
|
||||
@ -139,22 +98,13 @@ const formConfig = defineFormConfig([
|
||||
text: '主轴对齐',
|
||||
type: 'radioGroup',
|
||||
childType: 'button',
|
||||
labelWidth: '90px',
|
||||
iconSize: '24px',
|
||||
labelWidth: '68px',
|
||||
options: [
|
||||
{ value: 'flex-start', icon: markRaw(JustifyContentFlexStart), tooltip: '左对齐 flex-start' },
|
||||
{ value: 'flex-end', icon: markRaw(JustifyContentFlexEnd), tooltip: '右对齐 flex-end' },
|
||||
{ value: 'center', icon: markRaw(JustifyContentCenter), tooltip: '居中 center' },
|
||||
{
|
||||
value: 'space-between',
|
||||
icon: markRaw(JustifyContentSpaceBetween),
|
||||
tooltip: '两端对齐 space-between',
|
||||
},
|
||||
{
|
||||
value: 'space-around',
|
||||
icon: markRaw(JustifyContentSpaceAround),
|
||||
tooltip: '横向平分 space-around',
|
||||
},
|
||||
{ value: 'space-between', icon: markRaw(JustifyContentSpaceBetween), tooltip: '两端对齐 space-between' },
|
||||
{ value: 'space-around', icon: markRaw(JustifyContentSpaceAround), tooltip: '横向平分 space-around' },
|
||||
],
|
||||
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
},
|
||||
@ -163,22 +113,13 @@ const formConfig = defineFormConfig([
|
||||
text: '辅轴对齐',
|
||||
type: 'radioGroup',
|
||||
childType: 'button',
|
||||
labelWidth: '90px',
|
||||
iconSize: '24px',
|
||||
labelWidth: '68px',
|
||||
options: [
|
||||
{ value: 'flex-start', icon: markRaw(AlignItemsFlexStart), tooltip: '左对齐 flex-start' },
|
||||
{ value: 'flex-end', icon: markRaw(AlignItemsFlexEnd), tooltip: '右对齐 flex-end' },
|
||||
{ value: 'center', icon: markRaw(AlignItemsCenter), tooltip: '居中 center' },
|
||||
{
|
||||
value: 'space-between',
|
||||
icon: markRaw(AlignItemsSpaceBetween),
|
||||
tooltip: '两端对齐 space-between',
|
||||
},
|
||||
{
|
||||
value: 'space-around',
|
||||
icon: markRaw(AlignItemsSpaceAround),
|
||||
tooltip: '横向平分 space-around',
|
||||
},
|
||||
{ value: 'flex-start', icon: markRaw(JustifyContentFlexStart), tooltip: '左对齐 flex-start' },
|
||||
{ value: 'flex-end', icon: markRaw(JustifyContentFlexEnd), tooltip: '右对齐 flex-end' },
|
||||
{ value: 'center', icon: markRaw(JustifyContentCenter), tooltip: '居中 center' },
|
||||
{ value: 'space-between', icon: markRaw(JustifyContentSpaceBetween), tooltip: '两端对齐 space-between' },
|
||||
{ value: 'space-around', icon: markRaw(JustifyContentSpaceAround), tooltip: '横向平分 space-around' },
|
||||
],
|
||||
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
},
|
||||
@ -186,9 +127,8 @@ const formConfig = defineFormConfig([
|
||||
name: 'flexWrap',
|
||||
text: '换行',
|
||||
type: 'radioGroup',
|
||||
childType: displayTheme.value !== 'magic-admin' ? 'button' : 'default',
|
||||
labelWidth: '90px',
|
||||
iconSize: '24px',
|
||||
childType: 'button',
|
||||
labelWidth: '68px',
|
||||
options: [
|
||||
{ value: 'nowrap', text: '不换行', tooltip: '不换行 nowrap' },
|
||||
{ value: 'wrap', text: '正换行', tooltip: '第一行在上方 wrap' },
|
||||
@ -201,22 +141,17 @@ const formConfig = defineFormConfig([
|
||||
items: [
|
||||
{
|
||||
name: 'width',
|
||||
text: '宽度(px)',
|
||||
labelWidth: '90px',
|
||||
text: '宽度',
|
||||
labelWidth: '68px',
|
||||
type: 'data-source-field-select',
|
||||
fieldConfig: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
items: [
|
||||
{
|
||||
name: 'height',
|
||||
text: '高度(px)',
|
||||
labelWidth: '90px',
|
||||
text: '高度',
|
||||
labelWidth: '68px',
|
||||
type: 'data-source-field-select',
|
||||
fieldConfig: {
|
||||
type: 'text',
|
||||
@ -231,7 +166,7 @@ const formConfig = defineFormConfig([
|
||||
type: 'data-source-field-select',
|
||||
text: 'overflow',
|
||||
name: 'overflow',
|
||||
labelWidth: '90px',
|
||||
labelWidth: '68px',
|
||||
checkStrictly: false,
|
||||
dataSourceFieldType: ['string'],
|
||||
fieldConfig: {
|
||||
@ -249,16 +184,11 @@ const formConfig = defineFormConfig([
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
items: [
|
||||
{
|
||||
type: 'data-source-field-select',
|
||||
text: '透明度(%)',
|
||||
text: '透明度',
|
||||
name: 'opacity',
|
||||
labelWidth: '90px',
|
||||
labelWidth: '68px',
|
||||
dataSourceFieldType: ['string', 'number'],
|
||||
fieldConfig: {
|
||||
type: 'text',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -32,9 +32,6 @@ export { DepTargetType } from '@tmagic/core';
|
||||
export * from './type';
|
||||
export * from './hooks';
|
||||
export * from './utils';
|
||||
// `isGlobalFlat` 同时在 `@tmagic/design` 和 `./utils/config` 中存在,
|
||||
// 通过显式重导一次消除 `export *` 带来的歧义,并固定使用 editor 自己那份。
|
||||
export { isGlobalFlat } from './utils/config';
|
||||
export { default as TMagicEditor } from './Editor.vue';
|
||||
export { default as TMagicCodeEditor } from './layouts/CodeEditor.vue';
|
||||
export { default as editorService } from './services/editor';
|
||||
|
||||
@ -333,11 +333,11 @@ export const initServiceEvents = (
|
||||
Promise.all(
|
||||
nodes.map((node) => {
|
||||
if (node.type === NodeType.ROOT) {
|
||||
return Promise.resolve(true);
|
||||
return Promise.resolve();
|
||||
}
|
||||
return depService.collectIdle([node], { pageId: getPageIdByNode(node) }, deep, type);
|
||||
}),
|
||||
).then((results) => results.every(Boolean));
|
||||
);
|
||||
|
||||
watch(
|
||||
() => editorService.get('stage'),
|
||||
@ -351,7 +351,7 @@ export const initServiceEvents = (
|
||||
|
||||
if (!node) return;
|
||||
|
||||
if (!(await collectIdle([node], true, DepTargetType.DATA_SOURCE))) return;
|
||||
await collectIdle([node], true, DepTargetType.DATA_SOURCE);
|
||||
updateStageNode(node);
|
||||
});
|
||||
},
|
||||
@ -494,10 +494,8 @@ export const initServiceEvents = (
|
||||
|
||||
// 新增节点,收集依赖
|
||||
const nodeAddHandler = (nodes: MComponent[]) => {
|
||||
collectIdle(nodes, true).then((completed) => {
|
||||
if (completed) {
|
||||
updateStageNodes(nodes);
|
||||
}
|
||||
collectIdle(nodes, true).then(() => {
|
||||
updateStageNodes(nodes);
|
||||
});
|
||||
};
|
||||
|
||||
@ -551,8 +549,8 @@ export const initServiceEvents = (
|
||||
if (needRecollectNodes.length) {
|
||||
// 有数据源依赖,需要等依赖重新收集完才更新stage
|
||||
const handler = async () => {
|
||||
if (!(await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE))) return;
|
||||
if (!(await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE_COND))) return;
|
||||
await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE);
|
||||
await collectIdle(needRecollectNodes, true, DepTargetType.DATA_SOURCE_COND);
|
||||
updateStageNodes(needRecollectNodes);
|
||||
};
|
||||
handler();
|
||||
@ -574,10 +572,8 @@ export const initServiceEvents = (
|
||||
|
||||
// 历史记录变化时,需要重新收集依赖
|
||||
const historyChangeHandler = (page: MPage | MPageFragment) => {
|
||||
collectIdle([page], true).then((completed) => {
|
||||
if (completed) {
|
||||
updateStageNode(page);
|
||||
}
|
||||
collectIdle([page], true).then(() => {
|
||||
updateStageNode(page);
|
||||
});
|
||||
};
|
||||
|
||||
@ -658,7 +654,7 @@ export const initServiceEvents = (
|
||||
if (Array.isArray(root?.items)) {
|
||||
depService.clearIdleTasks();
|
||||
|
||||
let collectIdlePromises: Promise<boolean>[] = [];
|
||||
let collectIdlePromises: Promise<void[]>[] = [];
|
||||
if (isModifyField) {
|
||||
depService.removeTarget(config.id, DepTargetType.DATA_SOURCE);
|
||||
depService.removeTarget(config.id, DepTargetType.DATA_SOURCE_COND);
|
||||
@ -683,15 +679,10 @@ export const initServiceEvents = (
|
||||
|
||||
collectIdlePromises = [collectIdle(root.items, true, DepTargetType.DATA_SOURCE_METHOD)];
|
||||
}
|
||||
const handler = async () => {
|
||||
const results = await Promise.all(collectIdlePromises);
|
||||
if (!results.every(Boolean)) return;
|
||||
|
||||
updateDataSourceSchema();
|
||||
await updateDsData();
|
||||
updateStageNodes(root.items);
|
||||
};
|
||||
handler();
|
||||
Promise.all(collectIdlePromises)
|
||||
.then(() => updateDataSourceSchema())
|
||||
.then(() => updateDsData())
|
||||
.then(() => updateStageNodes(root.items));
|
||||
}
|
||||
} else if (root?.dataSources) {
|
||||
updateDsData();
|
||||
@ -715,12 +706,11 @@ export const initServiceEvents = (
|
||||
const nodeIds = Object.keys(root.dataSourceDeps?.[id] || {});
|
||||
const nodes = getNodes(nodeIds, root.items);
|
||||
|
||||
const results = await Promise.all([
|
||||
await Promise.all([
|
||||
collectIdle(nodes, false, DepTargetType.DATA_SOURCE),
|
||||
collectIdle(nodes, false, DepTargetType.DATA_SOURCE_COND),
|
||||
collectIdle(nodes, false, DepTargetType.DATA_SOURCE_METHOD),
|
||||
]);
|
||||
if (!results.every(Boolean)) return;
|
||||
|
||||
updateDataSourceSchema();
|
||||
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
<template>
|
||||
<div
|
||||
ref="content"
|
||||
:class="['m-editor', theme ? `m-editor--${theme}` : '', theme ? `m-theme--${theme}` : '']"
|
||||
style="min-width: 900px"
|
||||
>
|
||||
<div class="m-editor" ref="content" style="min-width: 900px">
|
||||
<slot name="header"></slot>
|
||||
|
||||
<slot name="nav"></slot>
|
||||
@ -101,8 +97,6 @@ const props = defineProps<{
|
||||
pageFilterFunction?: (_page: MPage | MPageFragment, _keyword: string) => boolean;
|
||||
/** 是否隐藏左侧面板 */
|
||||
hideSidebar?: boolean;
|
||||
/** 主题名称,会在根节点追加 `m-editor--<theme>` 修饰类 */
|
||||
theme?: string;
|
||||
}>();
|
||||
|
||||
const codeOptions = inject('codeOptions', {});
|
||||
|
||||
@ -49,13 +49,14 @@ import { Document as DocumentIcon } from '@element-plus/icons-vue';
|
||||
|
||||
import { TMagicButton, tMagicMessage, TMagicScrollbar } from '@tmagic/design';
|
||||
import type { ContainerChangeEventData, FormConfig, FormState, FormValue } from '@tmagic/form';
|
||||
import { MForm, validateForm } from '@tmagic/form';
|
||||
import { MForm } from '@tmagic/form';
|
||||
import { filterXSS } from '@tmagic/utils';
|
||||
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
import { ENABLE_PROPS_FORM_VALIDATE } from '@editor/editorProps';
|
||||
import { useEditorContentHeight } from '@editor/hooks/use-editor-content-height';
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
import { validatePropsForm } from '@editor/utils/props';
|
||||
|
||||
import CodeEditor from '../CodeEditor.vue';
|
||||
|
||||
@ -161,19 +162,14 @@ const saveCode = async (values: any) => {
|
||||
// 做一次静默校验(不复用、也不污染页面上正在展示的表单),并将校验结果(错误信息)随提交
|
||||
// 一并抛给上层记录,使源码保存的错误状态与表单编辑保持一致。
|
||||
try {
|
||||
const error = await validateForm({
|
||||
const error = await validatePropsForm({
|
||||
config: props.config,
|
||||
values: newValues,
|
||||
appContext: internalInstance?.appContext ?? null,
|
||||
services,
|
||||
stage: stage.value,
|
||||
typeMatchValid: true,
|
||||
initValues: newValues,
|
||||
// 将当前组件实例的 provides(含 Editor 顶层的 services / codeOptions 等组件级 provide)
|
||||
// 合入 appContext,使临时 MForm 中的编辑器字段组件(DataSourceInput 等)能正常 inject
|
||||
appContext: internalInstance?.appContext ? { ...internalInstance?.appContext, provides: { services } } : null,
|
||||
extendState: (state) => {
|
||||
if (configFormRef.value?.formState) {
|
||||
return { ...(configFormRef.value?.formState || {}) };
|
||||
}
|
||||
return props.extendState?.(state) || {};
|
||||
},
|
||||
extendState: props.extendState,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
|
||||
@ -111,31 +111,14 @@ const styleFormConfig = [
|
||||
},
|
||||
];
|
||||
|
||||
// 用单调递增序号标记每次 init 调用,只让"最新一次"的 await 结果落到 ref 上。
|
||||
// 避免节点快速切换时多个 init 并发 + 解析顺序错乱导致 stale config 覆盖最新选中节点,
|
||||
// 以及组件已卸载 / FormPanel 正在 remount 的中间态下写 ref 触发 Vue setRef 把 __vnode
|
||||
// 设到 null 上的报错(TypeError: Cannot set properties of null (setting '__vnode'))。
|
||||
let initSeq = 0;
|
||||
let mounted = true;
|
||||
|
||||
const init = async () => {
|
||||
initSeq += 1;
|
||||
const seq = initSeq;
|
||||
|
||||
if (!node.value) {
|
||||
if (seq !== initSeq || !mounted) return;
|
||||
curFormConfig.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const type = node.value.type || (node.value.items ? 'container' : 'text');
|
||||
const config = await propsService.getPropsConfig(type, { node: node.value });
|
||||
|
||||
// 期间被新一次 init 取代 / 组件已卸载,丢弃本次结果
|
||||
if (seq !== initSeq || !mounted) return;
|
||||
if (!node.value) return;
|
||||
|
||||
curFormConfig.value = config;
|
||||
curFormConfig.value = await propsService.getPropsConfig(type, { node: node.value });
|
||||
values.value = node.value;
|
||||
};
|
||||
|
||||
@ -143,7 +126,6 @@ watchEffect(init);
|
||||
propsService.on('props-configs-change', init);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
mounted = false;
|
||||
propsService.off('props-configs-change', init);
|
||||
});
|
||||
|
||||
@ -164,25 +146,17 @@ const submit = async (
|
||||
};
|
||||
|
||||
if (v.style) {
|
||||
// 空字符串样式值表示「清除该样式」,需保留才能在 doUpdate 的 mergeWith 中覆盖旧值。
|
||||
if (eventData) {
|
||||
// 表单编辑:先过滤掉空字符串(避免表单默认空值污染 DSL),
|
||||
// 再按 changeRecords 恢复被主动清空的字段。
|
||||
Object.entries(v.style).forEach(([key, value]) => {
|
||||
if (value !== '' && newValue.style) {
|
||||
newValue.style[key] = value;
|
||||
}
|
||||
});
|
||||
Object.entries(v.style).forEach(([key, value]) => {
|
||||
if (value !== '' && newValue.style) {
|
||||
newValue.style[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
eventData.changeRecords?.forEach((record) => {
|
||||
if (record.propPath?.startsWith('style') && record.value === '') {
|
||||
setValueByKeyPath(record.propPath, record.value, newValue);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 源码编辑器保存(无 eventData):style 原样保留,其中的空字符串视为用户主动清除该样式。
|
||||
newValue.style = { ...v.style };
|
||||
}
|
||||
eventData?.changeRecords?.forEach((record) => {
|
||||
if (record.propPath?.startsWith('style') && record.value === '') {
|
||||
setValueByKeyPath(record.propPath, record.value, newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 区分操作途径:表单字段编辑(MForm @change)会带上 eventData(含 changeRecords);
|
||||
|
||||
@ -155,7 +155,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { Box, Close, Coin, EditPen, Finished } from '@element-plus/icons-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';
|
||||
@ -253,7 +253,7 @@ const getItemConfig = (data: SideItem): SideComponent => {
|
||||
[SideItemKey.COMPONENT_LIST]: {
|
||||
$key: SideItemKey.COMPONENT_LIST,
|
||||
type: 'component',
|
||||
icon: Box,
|
||||
icon: Goods,
|
||||
text: '组件',
|
||||
component: ComponentListPanel,
|
||||
slots: {},
|
||||
@ -261,7 +261,7 @@ const getItemConfig = (data: SideItem): SideComponent => {
|
||||
layer: {
|
||||
$key: SideItemKey.LAYER,
|
||||
type: 'component',
|
||||
icon: Finished,
|
||||
icon: List,
|
||||
text: '已选组件',
|
||||
props: {
|
||||
layerContentMenu: props.layerContentMenu,
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type App } from 'vue';
|
||||
import type { App } from 'vue';
|
||||
|
||||
import type { DesignPluginOptions } from '@tmagic/design';
|
||||
import designPlugin from '@tmagic/design';
|
||||
@ -56,8 +56,6 @@ const defaultInstallOpt: EditorInstallOptions = {
|
||||
customCreateMonacoEditor: (monaco, codeEditorEl, options) => monaco.editor.create(codeEditorEl, options),
|
||||
customCreateMonacoDiffEditor: (monaco, codeEditorEl, options) =>
|
||||
monaco.editor.createDiffEditor(codeEditorEl, options),
|
||||
|
||||
flat: false,
|
||||
};
|
||||
|
||||
export default {
|
||||
@ -71,6 +69,7 @@ export default {
|
||||
|
||||
app.config.globalProperties.$TMAGIC_EDITOR = option;
|
||||
setEditorConfig(option);
|
||||
|
||||
app.component(`${Editor.name || 'MEditor'}`, Editor);
|
||||
app.component('magic-code-editor', CodeEditor);
|
||||
app.component('m-fields-ui-select', uiSelect);
|
||||
|
||||
@ -42,21 +42,6 @@ interface State {
|
||||
|
||||
type StateKey = keyof State;
|
||||
|
||||
/**
|
||||
* 一次 collectIdle 调用对应一个批次。批次自行统计入队/完成的任务数,在自身任务全部完成或被中断时
|
||||
* 结算对应的 Promise,避免多次快速触发时共用 idleTask 的 finish 事件造成的串扰、提前 resolve,
|
||||
* 以及 clearTasks 后 Promise 永不 resolve(collecting 卡死、监听器泄漏)。
|
||||
*/
|
||||
interface CollectBatch {
|
||||
nodes: MNode[];
|
||||
deep: boolean;
|
||||
pending: number;
|
||||
dsPending: number;
|
||||
collectedEmitted: boolean;
|
||||
dsSettled: boolean;
|
||||
resolve: (completed: boolean) => void;
|
||||
}
|
||||
|
||||
class Dep extends BaseService {
|
||||
private state = shallowReactive<State>({
|
||||
collecting: false,
|
||||
@ -69,12 +54,6 @@ class Dep extends BaseService {
|
||||
|
||||
private waitingWorker?: Promise<void>;
|
||||
|
||||
private resolveWaitingWorker?: () => void;
|
||||
|
||||
private workerGeneration = 0;
|
||||
|
||||
private activeBatches = new Set<CollectBatch>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
@ -158,44 +137,38 @@ class Dep extends BaseService {
|
||||
await this.waitingWorker;
|
||||
}
|
||||
|
||||
const batch: CollectBatch = {
|
||||
nodes,
|
||||
deep,
|
||||
pending: 0,
|
||||
dsPending: 0,
|
||||
collectedEmitted: false,
|
||||
dsSettled: false,
|
||||
resolve: () => {},
|
||||
};
|
||||
|
||||
this.set('collecting', true);
|
||||
let startTask = false;
|
||||
this.watcher.collectByCallback(nodes, type, ({ node, target }) => {
|
||||
this.enqueueTask(node, target, depExtendedData, deep, batch);
|
||||
startTask = true;
|
||||
|
||||
this.enqueueTask(node, target, depExtendedData, deep);
|
||||
});
|
||||
|
||||
// 没有命中任何 target,无需收集,直接完成
|
||||
if (batch.pending === 0) {
|
||||
this.emit('collected', nodes, deep);
|
||||
this.updateCollectingState();
|
||||
return true;
|
||||
}
|
||||
|
||||
this.activeBatches.add(batch);
|
||||
this.set('collecting', true);
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
batch.resolve = resolve;
|
||||
return new Promise<void>((resolve) => {
|
||||
if (!startTask) {
|
||||
this.emit('collected', nodes, deep);
|
||||
this.set('collecting', false);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
this.idleTask.once('finish', () => {
|
||||
this.emit('collected', nodes, deep);
|
||||
this.set('collecting', false);
|
||||
});
|
||||
this.idleTask.once('hight-level-finish', () => {
|
||||
this.emit('ds-collected', nodes, deep);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public collectByWorker(dsl: MApp) {
|
||||
this.set('collecting', true);
|
||||
this.workerGeneration += 1;
|
||||
const generation = this.workerGeneration;
|
||||
|
||||
const { promise, resolve: waitingResolve } = Promise.withResolvers<void>();
|
||||
|
||||
this.waitingWorker = promise;
|
||||
this.resolveWaitingWorker = waitingResolve;
|
||||
|
||||
return new Promise<Record<string, Record<string, DepData>>>((resolve) => {
|
||||
const worker = new Work();
|
||||
@ -209,11 +182,6 @@ class Dep extends BaseService {
|
||||
resolve({});
|
||||
};
|
||||
}).then((depsData) => {
|
||||
if (generation !== this.workerGeneration) {
|
||||
waitingResolve();
|
||||
return depsData;
|
||||
}
|
||||
|
||||
traverseTarget(this.watcher.getTargetsList(), (target) => {
|
||||
if (depsData[target.type]?.[target.id]) {
|
||||
target.deps = reactive(depsData[target.type][target.id]);
|
||||
@ -232,10 +200,6 @@ class Dep extends BaseService {
|
||||
this.emit('collected', dsl.items, true);
|
||||
this.emit('ds-collected', dsl.items, true);
|
||||
waitingResolve();
|
||||
if (this.waitingWorker === promise) {
|
||||
this.waitingWorker = undefined;
|
||||
this.resolveWaitingWorker = undefined;
|
||||
}
|
||||
|
||||
return depsData;
|
||||
});
|
||||
@ -269,7 +233,6 @@ class Dep extends BaseService {
|
||||
}
|
||||
|
||||
public clearIdleTasks() {
|
||||
this.abortActiveBatches();
|
||||
this.idleTask.clearTasks();
|
||||
}
|
||||
|
||||
@ -288,11 +251,6 @@ class Dep extends BaseService {
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.abortActiveBatches();
|
||||
this.workerGeneration += 1;
|
||||
this.resolveWaitingWorker?.();
|
||||
this.resolveWaitingWorker = undefined;
|
||||
this.waitingWorker = undefined;
|
||||
this.idleTask.clearTasks();
|
||||
|
||||
for (const type of Object.keys(this.watcher.getTargetsList())) {
|
||||
@ -328,99 +286,25 @@ class Dep extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private enqueueTask(
|
||||
node: MNode,
|
||||
target: Target,
|
||||
depExtendedData: DepExtendedData,
|
||||
deep: boolean,
|
||||
batch: CollectBatch,
|
||||
) {
|
||||
const isDataSource = target.type === DepTargetType.DATA_SOURCE;
|
||||
|
||||
batch.pending += 1;
|
||||
if (isDataSource) {
|
||||
batch.dsPending += 1;
|
||||
}
|
||||
|
||||
private enqueueTask(node: MNode, target: Target, depExtendedData: DepExtendedData, deep: boolean) {
|
||||
this.idleTask.enqueueTask(
|
||||
({ node, deep, target }) => {
|
||||
try {
|
||||
this.collectNode(node, target, depExtendedData, deep);
|
||||
} finally {
|
||||
this.onBatchTaskDone(batch, isDataSource);
|
||||
}
|
||||
this.collectNode(node, target, depExtendedData, deep);
|
||||
},
|
||||
{
|
||||
node,
|
||||
deep: false,
|
||||
target,
|
||||
},
|
||||
isDataSource,
|
||||
target.type === DepTargetType.DATA_SOURCE,
|
||||
);
|
||||
|
||||
if (deep && Array.isArray(node.items) && node.items.length) {
|
||||
node.items.forEach((item) => {
|
||||
this.enqueueTask(item, target, depExtendedData, deep, batch);
|
||||
this.enqueueTask(item, target, depExtendedData, deep);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onBatchTaskDone(batch: CollectBatch, isDataSource: boolean) {
|
||||
if (isDataSource) {
|
||||
batch.dsPending -= 1;
|
||||
// 数据源依赖收集完先 resolve,让 stage 尽快更新,其余依赖继续在后台收集
|
||||
if (batch.dsPending === 0) {
|
||||
this.settleBatchDs(batch);
|
||||
}
|
||||
}
|
||||
|
||||
batch.pending -= 1;
|
||||
if (batch.pending === 0) {
|
||||
this.finishBatch(batch);
|
||||
}
|
||||
}
|
||||
|
||||
private settleBatchDs(batch: CollectBatch) {
|
||||
if (batch.dsSettled) return;
|
||||
batch.dsSettled = true;
|
||||
this.emit('ds-collected', batch.nodes, batch.deep);
|
||||
batch.resolve(true);
|
||||
}
|
||||
|
||||
private finishBatch(batch: CollectBatch) {
|
||||
if (!batch.collectedEmitted) {
|
||||
batch.collectedEmitted = true;
|
||||
this.emit('collected', batch.nodes, batch.deep);
|
||||
}
|
||||
// 没有数据源任务的批次在此结算 Promise
|
||||
this.settleBatchDs(batch);
|
||||
this.activeBatches.delete(batch);
|
||||
this.updateCollectingState();
|
||||
}
|
||||
|
||||
/**
|
||||
* idleTask 被清空时,在途批次的任务不会再执行,必须主动结算,
|
||||
* 否则对应的 collectIdle Promise 永远不会 resolve(collecting 卡在 true、once 监听器泄漏)。
|
||||
* 收集被中断(通常紧跟一次全量重新收集),因此不再 emit collected/ds-collected,仅结算 Promise。
|
||||
*/
|
||||
private abortActiveBatches() {
|
||||
if (!this.activeBatches.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const batches = [...this.activeBatches];
|
||||
this.activeBatches.clear();
|
||||
|
||||
for (const batch of batches) {
|
||||
batch.resolve(false);
|
||||
}
|
||||
|
||||
this.updateCollectingState();
|
||||
}
|
||||
|
||||
private updateCollectingState() {
|
||||
this.set('collecting', this.activeBatches.size > 0);
|
||||
}
|
||||
}
|
||||
|
||||
export type DepService = Dep;
|
||||
|
||||
@ -550,16 +550,8 @@ class Editor extends BaseService {
|
||||
this.emit('change', {
|
||||
type: 'add',
|
||||
data: newNodes.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
|
||||
historySource,
|
||||
doNotPushHistory,
|
||||
});
|
||||
|
||||
// 页面 / 页面片新增不入历史栈(见上方 isPage / isPageFragment 分支),这里合并补发一次结构变更通知
|
||||
const addedPages = newNodes.filter((node) => isPage(node) || isPageFragment(node)) as (MPage | MPageFragment)[];
|
||||
if (addedPages.length) {
|
||||
historyService.notifyPageStructureChange({ add: addedPages, remove: [] });
|
||||
}
|
||||
|
||||
return Array.isArray(addNode) ? newNodes : newNodes[0];
|
||||
}
|
||||
|
||||
@ -700,13 +692,7 @@ class Editor extends BaseService {
|
||||
}
|
||||
|
||||
this.emit('remove', nodes);
|
||||
this.emit('change', { type: 'remove', data: changeItems, historySource, doNotPushHistory });
|
||||
|
||||
// 页面 / 页面片删除不入历史栈(见上方 isPage / isPageFragment 分支),这里合并补发一次结构变更通知
|
||||
const removedPages = nodes.filter((node) => isPage(node) || isPageFragment(node)) as (MPage | MPageFragment)[];
|
||||
if (removedPages.length) {
|
||||
historyService.notifyPageStructureChange({ add: [], remove: removedPages });
|
||||
}
|
||||
this.emit('change', { type: 'remove', data: changeItems });
|
||||
}
|
||||
|
||||
public async doUpdate(
|
||||
@ -860,8 +846,6 @@ class Editor extends BaseService {
|
||||
this.emit('change', {
|
||||
type: 'update',
|
||||
data: updateData.map((node) => ({ node, page: this.getPageOfNode(node.newNode.id) })),
|
||||
historySource,
|
||||
doNotPushHistory,
|
||||
});
|
||||
return Array.isArray(config) ? updateData.map((item) => item.newNode) : updateData[0].newNode;
|
||||
}
|
||||
@ -1108,8 +1092,6 @@ class Editor extends BaseService {
|
||||
type: 'move-layer',
|
||||
data: [{ node, page: this.getPageOfNode(node.id) }],
|
||||
offset,
|
||||
historySource,
|
||||
doNotPushHistory,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1295,8 +1277,6 @@ class Editor extends BaseService {
|
||||
data: configs.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
|
||||
targetIndex,
|
||||
targetParent,
|
||||
historySource,
|
||||
doNotPushHistory,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1788,10 +1768,6 @@ class Editor extends BaseService {
|
||||
const nextMap = new Map(nextPages.map((p) => [`${p.id}`, p]));
|
||||
const indexInItems = (root: MApp, id: Id) => (root.items ?? []).findIndex((item) => `${item.id}` === `${id}`);
|
||||
|
||||
// 收集本次整体替换中增删的页面,循环结束后合并为一次结构变更通知(避免逐页派发多个事件)
|
||||
const addedPages: (MPage | MPageFragment)[] = [];
|
||||
const removedPages: (MPage | MPageFragment)[] = [];
|
||||
|
||||
nextPages.forEach((nextPage) => {
|
||||
const prevPage = prevMap.get(`${nextPage.id}`);
|
||||
if (!prevPage) {
|
||||
@ -1801,7 +1777,6 @@ class Editor extends BaseService {
|
||||
{ newSchema: cloneDeep(toRaw(nextPage)), parentId: nextRoot.id, index: indexInItems(nextRoot, nextPage.id) },
|
||||
source,
|
||||
);
|
||||
addedPages.push(nextPage);
|
||||
} else if (!isEqual(toRaw(prevPage), toRaw(nextPage))) {
|
||||
this.pushPageDiffStep(
|
||||
'update',
|
||||
@ -1820,13 +1795,8 @@ class Editor extends BaseService {
|
||||
{ oldSchema: cloneDeep(toRaw(prevPage)), parentId: preRoot.id, index: indexInItems(preRoot, prevPage.id) },
|
||||
source,
|
||||
);
|
||||
removedPages.push(prevPage);
|
||||
}
|
||||
});
|
||||
|
||||
if (addedPages.length || removedPages.length) {
|
||||
historyService.notifyPageStructureChange({ add: addedPages, remove: removedPages });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1908,12 +1878,10 @@ class Editor extends BaseService {
|
||||
/**
|
||||
* 应用历史操作(撤销 / 重做)
|
||||
*
|
||||
* 删除 / 更新类修改走 `editor.remove / update`,并通过 `doNotPushHistory` 阻止再次入栈、
|
||||
* `doNotSelect / doNotSwitchPage` 让选区由方法末尾的统一逻辑兜底;
|
||||
* 「重新插回节点」(撤销 remove / 重做 add)需按 step 记录的 parentId / index 精确还原,
|
||||
* 不走 this.add,由手工 splice + stage.add 完成后补发等价的 add / change 事件(见 emitHistoryInsertEvents)。
|
||||
* 所有 DSL 修改都走 `editor.add / remove / update`,并通过 `doNotPushHistory` 阻止再次入栈、
|
||||
* `doNotSelect / doNotSwitchPage` 让选区由方法末尾的统一逻辑兜底。
|
||||
*
|
||||
* 注意:这些路径都会发出 add / remove / update / change 事件,业务侧若需要区分"用户操作"与"撤销重做触发",
|
||||
* 注意:这些公开方法会发出 add / remove / update 事件,业务侧若需要区分"用户操作"与"撤销重做触发",
|
||||
* 请监听 `history-change` 事件配合判断。
|
||||
*
|
||||
* @param step 操作记录
|
||||
@ -1926,14 +1894,7 @@ class Editor extends BaseService {
|
||||
const stage = this.get('stage');
|
||||
if (!root) return;
|
||||
|
||||
// 撤销/重做内部复用 add/remove/update,透传被应用 step 上记录的 source,
|
||||
// 使其触发的 change 事件同样携带「历史来源」
|
||||
const commonOpts = {
|
||||
doNotSelect: true,
|
||||
doNotSwitchPage: true,
|
||||
doNotPushHistory: true,
|
||||
historySource: step.source,
|
||||
} as const;
|
||||
const commonOpts = { doNotSelect: true, doNotSwitchPage: true, doNotPushHistory: true } as const;
|
||||
|
||||
switch (step.opType) {
|
||||
case 'add': {
|
||||
@ -1949,22 +1910,17 @@ class Editor extends BaseService {
|
||||
}
|
||||
} else {
|
||||
// 重做 add:按记录的 parentId / index 把节点重新插回父容器。
|
||||
// 按目标 index 升序逐个插入,先小后大避免索引漂移。
|
||||
// 不走 this.add:doAdd 依赖当前选中节点(无选区时直接抛错),且只能插到选中节点之后 / 末尾,
|
||||
// 无法按 step 记录的 parentId / index 精确还原,还会重算 style 破坏已记录的终态位置。
|
||||
// 按目标 index 升序逐个插入,先小后大避免索引漂移
|
||||
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
||||
const addedNodes: MNode[] = [];
|
||||
for (const { newSchema, parentId, index } of sorted) {
|
||||
if (!newSchema || parentId === undefined) continue;
|
||||
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
||||
if (parent?.items) {
|
||||
const addedNode = cloneDeep(newSchema);
|
||||
if (typeof index === 'number' && index >= 0 && index < parent.items.length) {
|
||||
parent.items.splice(index, 0, addedNode);
|
||||
parent.items.splice(index, 0, cloneDeep(newSchema));
|
||||
} else {
|
||||
parent.items.push(addedNode);
|
||||
parent.items.push(cloneDeep(newSchema));
|
||||
}
|
||||
addedNodes.push(addedNode);
|
||||
await stage?.add({
|
||||
config: cloneDeep(newSchema),
|
||||
parent: cloneDeep(parent),
|
||||
@ -1973,23 +1929,19 @@ class Editor extends BaseService {
|
||||
});
|
||||
}
|
||||
}
|
||||
this.emitHistoryInsertEvents(addedNodes, step.source);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'remove': {
|
||||
const items = step.diff ?? [];
|
||||
if (reverse) {
|
||||
// 撤销 remove:按原 index 升序逐个插回(先小后大避免索引漂移);不走 this.add 的原因同上
|
||||
// 撤销 remove:按原 index 升序逐个插回(先小后大避免索引漂移)
|
||||
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
||||
const addedNodes: MNode[] = [];
|
||||
for (const { oldSchema, parentId, index } of sorted) {
|
||||
if (!oldSchema || parentId === undefined) continue;
|
||||
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
||||
if (parent?.items) {
|
||||
const addedNode = cloneDeep(oldSchema);
|
||||
parent.items.splice(index ?? parent.items.length, 0, addedNode);
|
||||
addedNodes.push(addedNode);
|
||||
parent.items.splice(index ?? parent.items.length, 0, cloneDeep(oldSchema));
|
||||
await stage?.add({
|
||||
config: cloneDeep(oldSchema),
|
||||
parent: cloneDeep(parent),
|
||||
@ -1998,7 +1950,6 @@ class Editor extends BaseService {
|
||||
});
|
||||
}
|
||||
}
|
||||
this.emitHistoryInsertEvents(addedNodes, step.source);
|
||||
} else {
|
||||
// 重做 remove:再删一次
|
||||
for (const { oldSchema } of items) {
|
||||
@ -2038,7 +1989,7 @@ class Editor extends BaseService {
|
||||
return cloneDeep(reverse ? oldNode : newNode);
|
||||
});
|
||||
if (configs.length) {
|
||||
await this.update(configs, { doNotPushHistory: true, historySource: step.source });
|
||||
await this.update(configs, { doNotPushHistory: true });
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -2077,22 +2028,6 @@ class Editor extends BaseService {
|
||||
private selectedConfigExceptionHandler(config: MNode | Id): EditorNodeInfo {
|
||||
return resolveSelectedNode(config, (id) => this.getNodeInfo(id), this.state.root?.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销 remove / 重做 add 通过手工 splice 按 step 记录的 parentId / index 精确还原节点(不走 this.add,原因见调用处注释),
|
||||
* 这里补齐与 this.add 等价的 add / change 事件,保证撤销/重做路径的事件行为与正向操作一致。
|
||||
*/
|
||||
private emitHistoryInsertEvents(nodes: MNode[], source?: HistoryOpSource) {
|
||||
if (!nodes.length) return;
|
||||
this.emit('add', nodes);
|
||||
this.emit('change', {
|
||||
type: 'add',
|
||||
data: nodes.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
|
||||
historySource: source,
|
||||
// 撤销/重做本身不再写入历史记录
|
||||
doNotPushHistory: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type EditorService = Editor;
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
import { reactive } from 'vue';
|
||||
import type { Writable } from 'type-fest';
|
||||
|
||||
import type { Id, MPage, MPageFragment } from '@tmagic/core';
|
||||
import type { Id } from '@tmagic/core';
|
||||
import { guid } from '@tmagic/utils';
|
||||
|
||||
import type {
|
||||
@ -353,27 +353,6 @@ class History extends BaseService {
|
||||
this.emit('mark-saved', { kind: stepType, id });
|
||||
}
|
||||
|
||||
/**
|
||||
* 派发「页面 / 页面片结构变更」事件(`page-structure-change`)。
|
||||
*
|
||||
* 常规 `editorService.add` / `remove` 页面节点不会写入 `page` 历史栈(见 editor.add / remove 中
|
||||
* 对 isPage / isPageFragment 的分支),因此不会产生任何 historyService 事件。该方法用于在这些
|
||||
* 场景(以及 setRoot 整体替换 DSL 增删页面)下,向外统一通知页面结构的增删变化,供业务方感知。
|
||||
*
|
||||
* 一次操作涉及多个页面时,调用方应把本次增删的页面**合并为一个 change 一次性传入**,
|
||||
* 使一次操作只派发一个事件;`add` / `remove` 分别为本次新增与删除的页面列表(其一可为空数组)。
|
||||
*
|
||||
* @param change 本次结构变更:{ add: 新增的页面列表, remove: 删除的页面列表 }
|
||||
*/
|
||||
public notifyPageStructureChange(change: {
|
||||
add: (MPage | MPageFragment)[];
|
||||
remove: (MPage | MPageFragment)[];
|
||||
}): void {
|
||||
// 增删均为空时不派发,避免无意义通知
|
||||
if (!change.add.length && !change.remove.length) return;
|
||||
this.emit('page-structure-change', change);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把当前内存中的全部历史栈(页面 / 代码块 / 数据源 / 扩展类型)序列化后写入本地 IndexedDB。
|
||||
*
|
||||
|
||||
@ -49,14 +49,13 @@ const state = shallowReactive<UiState>({
|
||||
width: 375,
|
||||
height: 817,
|
||||
},
|
||||
// 先给 columnWidth 留占位,真正的 clamp 逻辑在 state 创建完成后
|
||||
// 用 state.minLeftColumnWidth / state.minRightColumnWidth 作为下限重新赋值 —
|
||||
// 这样以后从外部(比如 uiService.set('minLeftColumnWidth', ...))动态改最小宽度,
|
||||
// 只改一处 state 字段即可,不用同步 MIN_* 常量。
|
||||
columnWidth: {
|
||||
left: 0,
|
||||
left:
|
||||
storageService.getItem(LEFT_COLUMN_WIDTH_STORAGE_KEY, { protocol: Protocol.NUMBER }) || DEFAULT_LEFT_COLUMN_WIDTH,
|
||||
center: 0,
|
||||
right: 0,
|
||||
right:
|
||||
storageService.getItem(RIGHT_COLUMN_WIDTH_STORAGE_KEY, { protocol: Protocol.NUMBER }) ||
|
||||
DEFAULT_RIGHT_COLUMN_WIDTH,
|
||||
},
|
||||
minLeftColumnWidth: MIN_LEFT_COLUMN_WIDTH,
|
||||
minCenterColumnWidth: MIN_CENTER_COLUMN_WIDTH,
|
||||
@ -84,20 +83,6 @@ const state = shallowReactive<UiState>({
|
||||
},
|
||||
});
|
||||
|
||||
// 左右两栏宽度延后初始化:
|
||||
// - 优先取 storage 里用户上次拖到的值;storage 空时回落到 DEFAULT_*;
|
||||
// - 再用 state.min*ColumnWidth 兜底,防止历史 storage 里存的是老版本或用户拖过头
|
||||
// 留下的"比当前 min 还小"的数值。放到 state 之后写是因为初始化时无法在同一个
|
||||
// 对象字面量里读到自己的其它字段。
|
||||
state.columnWidth.left = Math.max(
|
||||
storageService.getItem(LEFT_COLUMN_WIDTH_STORAGE_KEY, { protocol: Protocol.NUMBER }) || DEFAULT_LEFT_COLUMN_WIDTH,
|
||||
state.minLeftColumnWidth,
|
||||
);
|
||||
state.columnWidth.right = Math.max(
|
||||
storageService.getItem(RIGHT_COLUMN_WIDTH_STORAGE_KEY, { protocol: Protocol.NUMBER }) || DEFAULT_RIGHT_COLUMN_WIDTH,
|
||||
state.minRightColumnWidth,
|
||||
);
|
||||
|
||||
const canUsePluginMethods = {
|
||||
async: ['zoom', 'calcZoom'] as const,
|
||||
sync: [] as const,
|
||||
|
||||
@ -1,40 +1,3 @@
|
||||
.m-fields-code-select {
|
||||
width: 100%;
|
||||
> .el-card.tmagic-design-card--flat {
|
||||
background-color: transparent;
|
||||
margin-bottom: 10px;
|
||||
> .el-card__body {
|
||||
padding-left: 0;
|
||||
.code-select-content {
|
||||
> .m-fields-group-list {
|
||||
> .tmagic-design-card--flat.el-card {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
margin-bottom: 10px;
|
||||
border: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-card__body {
|
||||
.tmagic-design-form-item {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
}
|
||||
.code-select-content {
|
||||
> .m-fields-group-list {
|
||||
> .tmagic-design-card--flat {
|
||||
> .el-card__header {
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.create-button {
|
||||
&.fullWidth {
|
||||
width: 100%;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
$theme-color: var(--el-color-primary, #2882e0);
|
||||
$theme-color: #2882e0;
|
||||
|
||||
$font-color: #313a40;
|
||||
$border-color: #d9dbdd;
|
||||
|
||||
@ -77,9 +77,9 @@
|
||||
margin-bottom: 5px;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-color-primary, #2882e0);
|
||||
background: #2882e0;
|
||||
color: #fff;
|
||||
border-color: var(--el-color-primary, #4e8be1);
|
||||
border-color: #4e8be1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,10 +5,6 @@
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&.data-source-field-img-upload,
|
||||
&.data-source-field-big-img-upload {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.m-fields-select-action-button {
|
||||
margin-left: 5px;
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
.m-container-text.display-conds-title {
|
||||
.tmagic-design-form-item {
|
||||
.el-form-item__label {
|
||||
color: #111;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
line-height: 24px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
@ -2,29 +2,8 @@
|
||||
width: 100%;
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.event-select-container {
|
||||
padding: 0 16px;
|
||||
> .el-card.tmagic-design-card--flat {
|
||||
> .el-card__header {
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
> .el-card__body {
|
||||
padding-bottom: 16px;
|
||||
.m-fields-group-list-footer {
|
||||
div {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
}
|
||||
.el-card.tmagic-design-card--flat {
|
||||
> .el-card__header {
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.event-select-code {
|
||||
margin-left: 20px;
|
||||
width: auto;
|
||||
@ -57,43 +36,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.event-select-container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.event-select-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
.event-select-title {
|
||||
color: #0f1113;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
.event-item-header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
.event-item-title {
|
||||
color: #0f1113;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.event-item-delete-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
.el-form-item {
|
||||
.el-form-item {
|
||||
&.is-error {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
&.is-error {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,10 +6,7 @@
|
||||
width: 100%;
|
||||
|
||||
&-content {
|
||||
// 与 nav-menu.scss 的 `--editor-nav-height` 联动:默认 fallback 到 SCSS
|
||||
// `$nav-height`(35px),主题覆盖时两处会同时切成同一个高度,保证 nav 和
|
||||
// 主内容区总高度 = 100%。
|
||||
height: calc(100% - var(--editor-nav-height, #{$nav-height}));
|
||||
height: calc(100% - #{$nav-height});
|
||||
}
|
||||
|
||||
&-framework-center {
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
color: #909399;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary, #409eff);
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,7 +96,7 @@
|
||||
// 故这里仅作用于「非合并组」的单步条目,避免与卡片样式互相干扰。
|
||||
&.is-current:not(.m-editor-history-list-group.is-merged) {
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
box-shadow: inset 2px 0 0 var(--el-color-primary, #409eff);
|
||||
box-shadow: inset 2px 0 0 #409eff;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(64, 158, 255, 0.16);
|
||||
@ -104,7 +104,7 @@
|
||||
|
||||
.m-editor-history-list-item-desc {
|
||||
font-weight: 600;
|
||||
color: var(--el-color-primary, #409eff);
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -177,11 +177,11 @@
|
||||
&.is-current {
|
||||
background-color: rgba(64, 158, 255, 0.08);
|
||||
border-color: rgba(64, 158, 255, 0.3);
|
||||
border-left-color: var(--el-color-primary, #409eff);
|
||||
border-left-color: #409eff;
|
||||
box-shadow: none; // 覆盖 .is-current 公共的 inset 阴影
|
||||
|
||||
.m-editor-history-list-group-head {
|
||||
color: var(--el-color-primary, #409eff);
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -216,7 +216,7 @@
|
||||
}
|
||||
|
||||
&.is-current {
|
||||
color: var(--el-color-primary, #409eff);
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
background-color: rgba(64, 158, 255, 0.08);
|
||||
border-radius: 3px;
|
||||
@ -231,7 +231,7 @@
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary, #409eff);
|
||||
background-color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@ -395,7 +395,7 @@
|
||||
border-radius: 2px;
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
color: var(--el-color-primary, #409eff);
|
||||
color: #409eff;
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
@ -471,7 +471,7 @@
|
||||
flex: 1 1 auto;
|
||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
font-size: 11px;
|
||||
color: var(--el-color-primary, #409eff);
|
||||
color: #409eff;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
@ -13,11 +13,7 @@
|
||||
font-weight: 400;
|
||||
box-sizing: border-box;
|
||||
margin: 0px;
|
||||
// 用 CSS 变量做主题化开关:默认没设 `--editor-nav-height` 时回落到
|
||||
// SCSS 变量 `$nav-height`(common/var.scss,35px);主题(如 magic-admin)
|
||||
// 只需在祖先节点声明 `--editor-nav-height: 40px` 即可运行时覆盖,避免
|
||||
// 走 `@use with` 那条对加载顺序极敏感的 SCSS 模块配置路径。
|
||||
flex: 0 0 var(--editor-nav-height, #{$nav-height});
|
||||
flex: 0 0 $nav-height;
|
||||
border-bottom: 1px solid #d8dee8;
|
||||
|
||||
> div {
|
||||
|
||||
@ -129,8 +129,7 @@
|
||||
> .m-container-tab {
|
||||
> .tmagic-design-tabs {
|
||||
> .el-tabs__content {
|
||||
margin-top: var(--el-tabs-header-height);
|
||||
padding-top: 15px;
|
||||
padding-top: 55px;
|
||||
}
|
||||
> .el-tabs__header.is-top {
|
||||
position: absolute;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
.border-box-container {
|
||||
display: flex;
|
||||
--unuse-border-style: dashed;
|
||||
.border-icon-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -29,32 +28,24 @@
|
||||
border-color: var(--el-color-success, var(--td-success-color, #2ba471));
|
||||
}
|
||||
&.active {
|
||||
border-color: var(--el-color-primary);
|
||||
border-width: 1px;
|
||||
// border-color: var(--el-color-primary, var(--td-brand-color, #0052d9));
|
||||
}
|
||||
&.center {
|
||||
border-width: 1px;
|
||||
border-color: var(--el-color-primary, var(--td-brand-color, #0052d9));
|
||||
}
|
||||
&.border-icon-top {
|
||||
border-top-width: 2px;
|
||||
border-style: solid var(--unuse-border-style) var(--unuse-border-style)
|
||||
var(--unuse-border-style);
|
||||
border-style: solid dashed dashed dashed;
|
||||
}
|
||||
&.border-icon-right {
|
||||
border-right-width: 2px;
|
||||
border-style: var(--unuse-border-style) solid var(--unuse-border-style)
|
||||
var(--unuse-border-style);
|
||||
border-style: dashed solid dashed dashed;
|
||||
}
|
||||
&.border-icon-bottom {
|
||||
border-bottom-width: 2px;
|
||||
border-style: var(--unuse-border-style) var(--unuse-border-style) solid
|
||||
var(--unuse-border-style);
|
||||
border-style: dashed dashed solid dashed;
|
||||
}
|
||||
&.border-icon-left {
|
||||
border-left-width: 2px;
|
||||
border-style: var(--unuse-border-style) var(--unuse-border-style)
|
||||
var(--unuse-border-style) solid;
|
||||
border-style: dashed dashed dashed solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,6 @@
|
||||
@use "./page-fragment-select.scss";
|
||||
@use "./data-source-field.scss";
|
||||
@use "./data-source-field-select.scss";
|
||||
@use "./display-conds.scss";
|
||||
@use "./scroll-bar.scss";
|
||||
@use "./ui-select.scss";
|
||||
@use "./layer-node-content.scss";
|
||||
|
||||
@ -1,214 +0,0 @@
|
||||
// magic-admin 主题:所有规则均以 `.m-editor.m-editor--magic-admin` 为前缀,
|
||||
// 仅在 `<MEditor theme="magic-admin" />` 时生效,避免污染默认样式。
|
||||
|
||||
@use "@tmagic/design/src/theme/themes/magic-admin/index.scss" as tMagicDesign;
|
||||
@use "@tmagic/table/src/theme/index.scss" as tMagicTable;
|
||||
@use "@tmagic/form/src/theme/themes/magic-admin/index.scss" as tMagicForm;
|
||||
@use "../../theme.scss";
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 仅作用于编辑器内部 DOM 的具体样式仍保留「双类」,避免误中 portal 上只挂 `m-theme--*`
|
||||
// 的节点。
|
||||
.m-editor.m-editor--magic-admin {
|
||||
// 主题级 CSS 变量入口:想覆盖 nav 高度等 token 时,改这里一行即可;
|
||||
// 走 CSS 级联,运行时切主题(换 `.m-editor--magic-admin` class)即可即时生效,
|
||||
// 不受 SCSS `@use` 加载顺序限制。fallback 值定义在 common/var.scss,未启用主题
|
||||
// 时自动回落 35px。
|
||||
--editor-nav-height: 40px;
|
||||
|
||||
.m-editor-sidebar {
|
||||
.m-editor-sidebar-header {
|
||||
// padding: 8px 0;
|
||||
width: 61px;
|
||||
padding-top: 4px;
|
||||
// sidebar 父容器是 `display: flex` (row),同级 `.m-editor-sidebar-content` 的 width
|
||||
// 是 `calc(100% - 40px)`(sidebar.scss:52,40 是老 header 宽度)。当 header 改成 61px
|
||||
// 后,两者之和会溢出 21px 左右,默认 `flex-shrink: 1` 会按权重压缩 header,最终渲染
|
||||
// 出 56.3px 而非 61px。这里强制 `flex-shrink: 0` 让 header 守住宽度,多余的部分由
|
||||
// content 侧承担(content 的 `100% - 40` 会自然让出)。
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid rgba(0, 84, 225, 0.1);
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.m-editor-sidebar-header-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
padding: 4px 2px;
|
||||
width: 52px;
|
||||
margin-bottom: 4px;
|
||||
box-sizing: border-box;
|
||||
background-color: transparent;
|
||||
color: #84909d;
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
> i {
|
||||
color: #84909d;
|
||||
font-size: 20px;
|
||||
}
|
||||
&.is-active {
|
||||
background-color: var(--el-button-hover-bg-color, rgb(230, 238, 253));
|
||||
color: var(--el-color-primary);
|
||||
&:hover {
|
||||
background-color: var(
|
||||
--el-button-hover-bg-color,
|
||||
rgb(230, 238, 253)
|
||||
);
|
||||
}
|
||||
> i {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.magic-editor-tab-panel-title {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.magic-editor-tab-panel-title {
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
font-weight: 400;
|
||||
margin: 0 4px;
|
||||
width: 52px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.m-editor-sidebar-content {
|
||||
width: calc(100% - 61px);
|
||||
}
|
||||
}
|
||||
.m-editor-props-panel .el-input__wrapper {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.border-box-container .border-icon-container .border-icon {
|
||||
border-color: #ccc;
|
||||
--unuse-border-style: solid;
|
||||
&.configured {
|
||||
border-color: var(--el-color-success, var(--td-success-color, #2ba471));
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--el-color-primary);
|
||||
background-color: rgba(0, 84, 225, 0.1);
|
||||
&.center {
|
||||
border-width: 2px;
|
||||
}
|
||||
// 命中 `.border-icon-<side>` 时只保留该侧 2px,其余三边清零。
|
||||
// 用 `@each` 循环避免四面手抄;后续要加禁用、hover 等状态也可以照搬这套结构。
|
||||
@each $side in (top, right, bottom, left) {
|
||||
&.border-icon-#{$side} {
|
||||
border-width: 0;
|
||||
border-#{$side}-width: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.center {
|
||||
border-width: 2px;
|
||||
}
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.m-container-code-select {
|
||||
> .el-form-item:first-child {
|
||||
> .el-form-item__label:first-child > :not(.m-form-tip) {
|
||||
color: #111;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.m-editor.m-theme--magic-admin {
|
||||
.m-editor-props-form-panel-form {
|
||||
> .m-container-tab {
|
||||
> .tmagic-design-tabs {
|
||||
> .el-tabs__content {
|
||||
background-color: #fafafa;
|
||||
border-radius: 8px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.m-editor.m-theme--magic-admin {
|
||||
.m-fields-style-setter {
|
||||
border: 0;
|
||||
.tmagic-design-collapse-item {
|
||||
.el-collapse-item__wrap {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.m-editor.m-theme--magic-admin {
|
||||
.magic-right-panel-tabs-top {
|
||||
> .el-tabs {
|
||||
> .el-tabs__header {
|
||||
.el-tabs__nav-wrap.is-top {
|
||||
.el-tabs__nav.is-top {
|
||||
.el-tabs__active-bar {
|
||||
display: none;
|
||||
}
|
||||
.el-tabs__item {
|
||||
&:not(:last-child)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 12px;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
}
|
||||
}
|
||||
}
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.m-editor.m-theme--magic-admin {
|
||||
.m-editor-nav-menu {
|
||||
.menu-right,
|
||||
.menu-left {
|
||||
> .menu-item {
|
||||
> button {
|
||||
color: #111;
|
||||
}
|
||||
}
|
||||
}
|
||||
.menu-item {
|
||||
margin-right: 8px;
|
||||
.el-button--small {
|
||||
font-size: 14px;
|
||||
}
|
||||
> button {
|
||||
> i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,6 @@
|
||||
margin-right: 3px;
|
||||
}
|
||||
span {
|
||||
color: var(--el-color-primary, #2882e0);
|
||||
color: #2882e0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,7 +140,6 @@ export type BeforeAdd = (config: MNode, parent: MContainer) => Promise<MNode> |
|
||||
export type GetConfig = (config: FormConfig) => Promise<FormConfig> | FormConfig;
|
||||
|
||||
export interface EditorInstallOptions {
|
||||
flat?: boolean;
|
||||
parseDSL: <T = any>(dsl: string) => T;
|
||||
customCreateMonacoEditor: (
|
||||
monaco: typeof import('monaco-editor'),
|
||||
@ -1339,37 +1338,17 @@ export interface EditorUpdateChangeItem {
|
||||
page: StoreState['page'];
|
||||
}
|
||||
|
||||
/** {@link EditorEvents.change} 各操作类型共有的历史相关字段。 */
|
||||
export interface EditorChangeEventHistoryMeta {
|
||||
/**
|
||||
* 本次变更的「历史来源」(调用 DSL 操作时传入的 {@link HistoryOpOptions.historySource},
|
||||
* 撤销 / 重做时则为被应用 step 上记录的 `source`),未携带时为 undefined。
|
||||
*/
|
||||
historySource?: HistoryOpSource;
|
||||
/**
|
||||
* 本次操作是否未写入历史记录(即调用时传入的 {@link HistoryOpOptions.doNotPushHistory},缺省为 false);
|
||||
* 撤销 / 重做路径补发的事件恒为 true(撤销/重做本身不再入栈)。
|
||||
*/
|
||||
doNotPushHistory?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link EditorEvents.change} 的回调参数:以 `type` 区分操作类型,并携带对应的操作内容。
|
||||
* `data` 为本次变更的节点列表,每项包含 node 及其所属的 page(可能为 null);
|
||||
* `move-layer` 额外带层级偏移 `offset`,`drag-to` 额外带目标位置 `targetIndex` / `targetParent`;
|
||||
* 历史相关字段(historySource / doNotPushHistory)见 {@link EditorChangeEventHistoryMeta}。
|
||||
* `move-layer` 额外带层级偏移 `offset`,`drag-to` 额外带目标位置 `targetIndex` / `targetParent`。
|
||||
*/
|
||||
export type EditorChangeEvent =
|
||||
| ({ type: 'add'; data: EditorChangeItem[] } & EditorChangeEventHistoryMeta)
|
||||
| ({ type: 'remove'; data: EditorChangeItem[] } & EditorChangeEventHistoryMeta)
|
||||
| ({ type: 'update'; data: EditorUpdateChangeItem[] } & EditorChangeEventHistoryMeta)
|
||||
| ({ type: 'move-layer'; data: EditorChangeItem[]; offset: number | LayerOffset } & EditorChangeEventHistoryMeta)
|
||||
| ({
|
||||
type: 'drag-to';
|
||||
data: EditorChangeItem[];
|
||||
targetIndex: number;
|
||||
targetParent: MContainer;
|
||||
} & EditorChangeEventHistoryMeta);
|
||||
| { type: 'add'; data: EditorChangeItem[] }
|
||||
| { type: 'remove'; data: EditorChangeItem[] }
|
||||
| { type: 'update'; data: EditorUpdateChangeItem[] }
|
||||
| { type: 'move-layer'; data: EditorChangeItem[]; offset: number | LayerOffset }
|
||||
| { type: 'drag-to'; data: EditorChangeItem[]; targetIndex: number; targetParent: MContainer };
|
||||
// #endregion EditorChangeEvent
|
||||
|
||||
export interface HistoryEvents {
|
||||
@ -1385,12 +1364,6 @@ export interface HistoryEvents {
|
||||
'mark-saved': [{ kind: HistoryStepType; id?: Id }];
|
||||
clear: [{ id: Id; stepType: HistoryStepType }];
|
||||
'marker-change': [{ id: Id; marker: StepValue; stepType: HistoryStepType }];
|
||||
/**
|
||||
* 页面 / 页面片结构变更(新增 / 删除)时派发,见 {@link HistoryService.notifyPageStructureChange}。
|
||||
* 一次操作(add / remove / setRoot 整体替换)涉及多个页面时合并为**一个**事件;
|
||||
* `add` / `remove` 分别为本次新增与删除的页面列表(其一可为空数组)。
|
||||
*/
|
||||
'page-structure-change': [change: { add: (MPage | MPageFragment)[]; remove: (MPage | MPageFragment)[] }];
|
||||
}
|
||||
|
||||
export const canUsePluginMethods = {
|
||||
|
||||
@ -16,22 +16,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { EditorInstallOptions } from '@editor/type';
|
||||
|
||||
let $TMAGIC_EDITOR: EditorInstallOptions = {} as any;
|
||||
|
||||
// 用 ref 持有 flat 全局开关:const 引用本身不可变,符合 `import/no-mutable-exports`;
|
||||
// 通过 `.value` 改值仍保持模块级响应式语义,与 editor/plugin.ts、design/index.ts 里
|
||||
// 的同名变量写法对齐。
|
||||
const isGlobalFlat = ref(false);
|
||||
|
||||
const setEditorConfig = (option: EditorInstallOptions): void => {
|
||||
$TMAGIC_EDITOR = option;
|
||||
isGlobalFlat.value = option.flat ?? false;
|
||||
};
|
||||
|
||||
const getEditorConfig = <K extends keyof EditorInstallOptions>(key: K): EditorInstallOptions[K] => $TMAGIC_EDITOR[key];
|
||||
|
||||
export { getEditorConfig, isGlobalFlat, setEditorConfig };
|
||||
export { getEditorConfig, setEditorConfig };
|
||||
|
||||
@ -5,11 +5,10 @@ export const LEFT_COLUMN_WIDTH_STORAGE_KEY = '$MagicEditorLeftColumnWidthData';
|
||||
export const RIGHT_COLUMN_WIDTH_STORAGE_KEY = '$MagicEditorRightColumnWidthData';
|
||||
export const PROPS_PANEL_WIDTH_STORAGE_KEY = '$MagicEditorPropsPanelWidthData';
|
||||
|
||||
export const DEFAULT_LEFT_COLUMN_WIDTH = 345;
|
||||
export const DEFAULT_LEFT_COLUMN_WIDTH = 310;
|
||||
export const DEFAULT_RIGHT_COLUMN_WIDTH = 480;
|
||||
|
||||
// 2026-06-11 设计稿修改,固定左侧栏存在时最小宽度为 345
|
||||
export const MIN_LEFT_COLUMN_WIDTH = 345;
|
||||
export const MIN_LEFT_COLUMN_WIDTH = 200;
|
||||
export const MIN_CENTER_COLUMN_WIDTH = 400;
|
||||
export const MIN_RIGHT_COLUMN_WIDTH = 300;
|
||||
|
||||
|
||||
@ -243,7 +243,7 @@ export const resolveFieldByPath = (
|
||||
fields: DataSchema[] | undefined,
|
||||
fieldNames: string[],
|
||||
options: { skipNumberIndices?: boolean } = {},
|
||||
): { ok: boolean; field?: DataSchema; fields: DataSchema[]; failedName?: string } => {
|
||||
): { ok: boolean; field?: DataSchema; fields: DataSchema[] } => {
|
||||
let currentFields = fields || [];
|
||||
let field: DataSchema | undefined;
|
||||
|
||||
@ -252,11 +252,11 @@ export const resolveFieldByPath = (
|
||||
continue;
|
||||
}
|
||||
if (!currentFields.length) {
|
||||
return { ok: false, fields: currentFields, failedName: name };
|
||||
return { ok: false, fields: currentFields };
|
||||
}
|
||||
field = currentFields.find((item) => item.name === name);
|
||||
if (!field) {
|
||||
return { ok: false, fields: currentFields, failedName: name };
|
||||
return { ok: false, fields: currentFields };
|
||||
}
|
||||
currentFields = field.fields || [];
|
||||
}
|
||||
|
||||
@ -25,12 +25,6 @@ globalThis.requestIdleCallback =
|
||||
}, 1);
|
||||
};
|
||||
|
||||
globalThis.cancelIdleCallback =
|
||||
globalThis.cancelIdleCallback ||
|
||||
function (handle) {
|
||||
clearTimeout(handle as unknown as ReturnType<typeof setTimeout>);
|
||||
};
|
||||
|
||||
export class IdleTask<T = any> extends EventEmitter {
|
||||
private taskList: TaskList<T> = [];
|
||||
|
||||
|
||||
@ -16,6 +16,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { AppContext } from 'vue';
|
||||
|
||||
import {
|
||||
HookType,
|
||||
NODE_CONDS_KEY,
|
||||
@ -24,7 +27,18 @@ import {
|
||||
NODE_DISABLE_DATA_SOURCE_KEY,
|
||||
} from '@tmagic/core';
|
||||
import { tMagicMessage } from '@tmagic/design';
|
||||
import type { ChildConfig, DisplayCondsConfig, FormConfig, TabConfig, TabPaneConfig } from '@tmagic/form';
|
||||
import type {
|
||||
ChildConfig,
|
||||
DisplayCondsConfig,
|
||||
FormConfig,
|
||||
FormState,
|
||||
FormValue,
|
||||
TabConfig,
|
||||
TabPaneConfig,
|
||||
} from '@tmagic/form';
|
||||
import { validateForm } from '@tmagic/form';
|
||||
|
||||
import type { Services } from '@editor/type';
|
||||
|
||||
export const arrayOptions = [
|
||||
{ text: '包含', value: 'include' },
|
||||
@ -166,7 +180,6 @@ export const advancedTabConfig: TabPaneConfig = {
|
||||
name: NODE_DISABLE_CODE_BLOCK_KEY,
|
||||
text: '禁用代码块',
|
||||
type: 'switch',
|
||||
labelPosition: 'left',
|
||||
defaultValue: false,
|
||||
extra: '开启后,配置的代码块将不会被执行',
|
||||
},
|
||||
@ -174,16 +187,15 @@ export const advancedTabConfig: TabPaneConfig = {
|
||||
name: NODE_DISABLE_DATA_SOURCE_KEY,
|
||||
text: '禁用数据源',
|
||||
type: 'switch',
|
||||
labelPosition: 'left',
|
||||
defaultValue: false,
|
||||
extra: '开启后,组件内配置的数据源相关配置将不会被编译,显隐条件将失效',
|
||||
},
|
||||
{
|
||||
name: 'created',
|
||||
text: 'Created',
|
||||
text: 'created',
|
||||
labelPosition: 'top',
|
||||
type: 'code-select',
|
||||
titleExtra: '组件初始化时执行',
|
||||
extra: '组件初始化时执行',
|
||||
rules: [
|
||||
{ typeMatch: true, trigger: 'change' },
|
||||
{
|
||||
@ -198,10 +210,10 @@ export const advancedTabConfig: TabPaneConfig = {
|
||||
},
|
||||
{
|
||||
name: 'mounted',
|
||||
text: 'Mounted',
|
||||
text: 'mounted',
|
||||
labelPosition: 'top',
|
||||
type: 'code-select',
|
||||
titleExtra: '组件挂载到dom时执行',
|
||||
extra: '组件挂载到dom时执行',
|
||||
rules: [
|
||||
{ typeMatch: true, trigger: 'change' },
|
||||
{
|
||||
@ -216,8 +228,8 @@ export const advancedTabConfig: TabPaneConfig = {
|
||||
},
|
||||
{
|
||||
name: 'display',
|
||||
text: 'Display',
|
||||
titleExtra: '控制组件是否渲染,关系的代码块返回值为false时不渲染',
|
||||
text: 'display',
|
||||
extra: '控制组件是否渲染,关系的代码块返回值为false时不渲染',
|
||||
labelPosition: 'top',
|
||||
type: 'code-select',
|
||||
rules: [
|
||||
@ -251,17 +263,10 @@ export const displayTabConfig: TabPaneConfig<DisplayCondsConfig> = {
|
||||
extra: (_state, { model }) =>
|
||||
`条件成立时${model[NODE_CONDS_RESULT_KEY] ? '隐藏' : '显示'},不成立时${model[NODE_CONDS_RESULT_KEY] ? '显示' : '隐藏'};<br />同一条件组内的所有条件配置同时成立时表示该条件组成立,任意一个条件组成立时表示条件成立(条件组内为且的关系,条件组间为或的关系);<br />条件为空时表示成立;`,
|
||||
},
|
||||
{
|
||||
text: '条件组配置',
|
||||
static: true,
|
||||
className: 'display-conds-title',
|
||||
},
|
||||
{
|
||||
type: 'display-conds',
|
||||
name: NODE_CONDS_KEY,
|
||||
titlePrefix: '条件组',
|
||||
fixed: 'right',
|
||||
operateColWidth: 112,
|
||||
defaultValue: [],
|
||||
rules: [{ typeMatch: true }],
|
||||
},
|
||||
@ -298,13 +303,10 @@ export const fillConfig = (
|
||||
name: 'id',
|
||||
text: 'ID',
|
||||
type: 'text',
|
||||
// 走纯文本渲染,避免出现 disabled 的灰底输入框;append 仍正常显示。
|
||||
static: true,
|
||||
|
||||
disabled: true,
|
||||
append: {
|
||||
type: 'icon',
|
||||
text: 'https://vip.image.video.qpic.cn/vupload/20260615/36cf7e1781493669935.svg',
|
||||
extra: '复制',
|
||||
type: 'button',
|
||||
text: '复制',
|
||||
handler: (vm, { model }) => {
|
||||
navigator.clipboard
|
||||
.writeText(`${model.id}`)
|
||||
@ -337,7 +339,6 @@ export const fillConfig = (
|
||||
const tabConfig: TabConfig = {
|
||||
type: 'tab',
|
||||
labelWidth,
|
||||
className: 'magic-right-panel-tabs-top',
|
||||
items: [
|
||||
{
|
||||
title: '属性',
|
||||
@ -389,3 +390,86 @@ export const removeStyleDisplayConfig = (formConfig: FormConfig): FormConfig =>
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// #region ValidatePropsFormOptions
|
||||
/**
|
||||
* validatePropsForm 参数
|
||||
*/
|
||||
export interface ValidatePropsFormOptions {
|
||||
/** 组件属性表单配置 */
|
||||
config: FormConfig;
|
||||
/** 待校验的表单值 */
|
||||
values: FormValue;
|
||||
/**
|
||||
* 当前组件实例的 appContext(通常为 `getCurrentInstance()?.appContext`)。
|
||||
* 会与 services 一并合入临时 MForm 的 appContext,使编辑器字段组件(DataSourceInput 等)能正常 inject。
|
||||
*/
|
||||
appContext?: AppContext | null;
|
||||
/** 编辑器服务集合,注入到临时表单的 formState */
|
||||
services?: Services;
|
||||
/** stage 实例,注入到临时表单的 formState */
|
||||
stage?: any;
|
||||
/** 外部扩展的 formState */
|
||||
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
||||
/**
|
||||
* 调试模式,默认 `true`:以弹层形式可见地渲染表单,点击「确定」才触发校验。
|
||||
* 置为 `false` 时静默挂载后自动校验。
|
||||
*/
|
||||
debug?: boolean;
|
||||
typeMatchValid?: boolean;
|
||||
}
|
||||
// #endregion ValidatePropsFormOptions
|
||||
|
||||
/**
|
||||
* 对一份「组件属性表单配置 + 值」做一次独立的校验,**不复用也不污染页面上正在展示的表单**。
|
||||
*
|
||||
* 内部基于 `@tmagic/form` 的 `validateForm` 另建一个独立的 MForm 实例完成校验,并统一处理
|
||||
* 编辑器场景所需的上下文注入:将当前组件实例的 provides 合入 appContext,并向 formState 注入
|
||||
* stage / services 及外部扩展状态,保证校验规则依赖的上下文可用。
|
||||
*
|
||||
* 常用于源码编辑器保存后,对最新配置做一次校验,并将校验结果(错误信息)随提交一并抛给上层记录,
|
||||
* 使源码保存的错误状态与表单编辑保持一致。
|
||||
*
|
||||
* @returns 校验通过返回空字符串 `''`,否则返回以 `<br>` 拼接的错误文案。
|
||||
* 仅在初始化超时或挂载失败等异常情况下才会 reject。
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const error = await validatePropsForm({
|
||||
* config,
|
||||
* values,
|
||||
* appContext: getCurrentInstance()?.appContext,
|
||||
* services,
|
||||
* stage: editorService.get('stage'),
|
||||
* extendState,
|
||||
* });
|
||||
* if (error) {
|
||||
* // 配置不合法,error 为错误文案
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const validatePropsForm = ({
|
||||
config,
|
||||
values,
|
||||
appContext = null,
|
||||
services,
|
||||
stage,
|
||||
extendState,
|
||||
debug,
|
||||
typeMatchValid,
|
||||
}: ValidatePropsFormOptions): Promise<string> =>
|
||||
validateForm({
|
||||
config,
|
||||
debug,
|
||||
typeMatchValid,
|
||||
initValues: values,
|
||||
// 将当前组件实例的 provides(含 Editor 顶层的 services / codeOptions 等组件级 provide)
|
||||
// 合入 appContext,使临时 MForm 中的编辑器字段组件(DataSourceInput 等)能正常 inject
|
||||
appContext: appContext ? { ...appContext, provides: { services } } : null,
|
||||
// 与页面表单保持一致:注入 stage/services 及外部扩展状态,保证校验规则依赖的上下文可用
|
||||
extendState: async (state) => ({
|
||||
...((await extendState?.(state)) || {}),
|
||||
stage,
|
||||
services,
|
||||
}),
|
||||
});
|
||||
|
||||
@ -20,7 +20,6 @@ import type { DataSourceFieldType, DataSourceSchema, Id } from '@tmagic/core';
|
||||
import { NodeType } from '@tmagic/core';
|
||||
import { appendValidateSuggestion } from '@tmagic/design';
|
||||
import type { TypeMatchValidateContext, TypeMatchValidator } from '@tmagic/form';
|
||||
import { validateTypeMatch } from '@tmagic/form';
|
||||
import {
|
||||
DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX,
|
||||
DATA_SOURCE_SET_DATA_METHOD_NAME,
|
||||
@ -58,7 +57,7 @@ const stringifyExampleValue = (value: any): string => {
|
||||
};
|
||||
|
||||
// 参考建议中最多展示的可选值个数,超出以「等」省略。
|
||||
const MAX_SUGGESTION_OPTIONS = 20;
|
||||
const MAX_SUGGESTION_OPTIONS = 5;
|
||||
|
||||
/**
|
||||
* 生成「请使用以下某一个值:xxx;xxx」形式的参考建议;无可选值时返回空字符串(不追加建议)。
|
||||
@ -262,20 +261,16 @@ const validateDataSourceFieldPath = (
|
||||
|
||||
const ds = findDataSource(`${dsId}`);
|
||||
if (!ds) {
|
||||
return defaultMessage(options.message, `数据源(${dsId})不存在`, dataSourceIdSuggestion());
|
||||
return defaultMessage(options.message, '值不在可选项中', dataSourceIdSuggestion());
|
||||
}
|
||||
|
||||
if (!fieldNames.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { field, ok, fields, failedName } = resolveFieldByPath(ds.fields, fieldNames);
|
||||
const { field, ok } = resolveFieldByPath(ds.fields, fieldNames);
|
||||
if (!ok) {
|
||||
return defaultMessage(
|
||||
options.message,
|
||||
`数据源字段(${failedName})不存在`,
|
||||
listSuggestion(fields.map((item) => item.name)),
|
||||
);
|
||||
return defaultMessage(options.message, '值不在可选项中', dataSourceIdSuggestion());
|
||||
}
|
||||
|
||||
const allowedTypes = options.dataSourceFieldType || ['any'];
|
||||
@ -476,12 +471,11 @@ const isDataSourceFieldPathValue = (value: any, config: any): value is string[]
|
||||
return `${value[0]}`.startsWith(DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX);
|
||||
};
|
||||
|
||||
const validateDataSourceFieldSelect: TypeMatchValidator = (value, { mForm, message, props }) => {
|
||||
const validateDataSourceFieldSelect: TypeMatchValidator = (value, { message, props }) => {
|
||||
const config = props.config || {};
|
||||
|
||||
if (config.fieldConfig && !isDataSourceFieldPathValue(value, config)) {
|
||||
// 值不是数据源字段路径时,按 fieldConfig 的类型校验(与表单项自身 typeMatch 行为一致)
|
||||
return validateTypeMatch(value, mForm, { ...props, config: { name: config.name, ...config.fieldConfig } }, message);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
||||
|
||||
@ -4,27 +4,10 @@
|
||||
* Copyright (C) 2025 Tencent.
|
||||
*/
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
import { nextTick } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import ContentMenu from '@editor/components/ContentMenu.vue';
|
||||
|
||||
const roCallbacks: Array<() => void> = [];
|
||||
class FakeResizeObserver {
|
||||
public static disconnectSpy = vi.fn();
|
||||
public cb: () => void;
|
||||
constructor(cb: () => void) {
|
||||
this.cb = cb;
|
||||
roCallbacks.push(cb);
|
||||
}
|
||||
public observe() {}
|
||||
public unobserve() {}
|
||||
public disconnect() {
|
||||
FakeResizeObserver.disconnectSpy();
|
||||
}
|
||||
}
|
||||
(globalThis as any).ResizeObserver = FakeResizeObserver;
|
||||
|
||||
const provideServices = () => ({
|
||||
global: {
|
||||
provide: {
|
||||
@ -39,8 +22,6 @@ const provideServices = () => ({
|
||||
describe('ContentMenu.vue', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
roCallbacks.length = 0;
|
||||
FakeResizeObserver.disconnectSpy.mockClear();
|
||||
});
|
||||
|
||||
test('show 后触发 show 事件', async () => {
|
||||
@ -86,62 +67,6 @@ describe('ContentMenu.vue', () => {
|
||||
expect((wrapper.vm as any).menuPosition.top).toBeLessThanOrEqual(100);
|
||||
});
|
||||
|
||||
test('菜单大小动态变化后修正位置避免超出可视范围', async () => {
|
||||
// jsdom 中 clientWidth/clientHeight 默认为 0,先 mock 一个足够大的视口,避免 show 时位置被修正
|
||||
Object.defineProperty(document.body, 'clientHeight', { value: 1000, configurable: true });
|
||||
Object.defineProperty(document.body, 'clientWidth', { value: 1000, configurable: true });
|
||||
|
||||
const wrapper = mount(ContentMenu as any, {
|
||||
...provideServices(),
|
||||
props: { menuData: [{ id: '1', type: 'button', text: 'a' }] as any },
|
||||
attachTo: document.body,
|
||||
});
|
||||
(wrapper.vm as any).show({ clientX: 90, clientY: 90 });
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect((wrapper.vm as any).menuPosition.top).toBe(90);
|
||||
expect((wrapper.vm as any).menuPosition.left).toBe(90);
|
||||
|
||||
// 视口缩小为 100x100
|
||||
Object.defineProperty(document.body, 'clientHeight', { value: 100, configurable: true });
|
||||
Object.defineProperty(document.body, 'clientWidth', { value: 100, configurable: true });
|
||||
|
||||
const menuEl = wrapper.find('.magic-editor-content-menu').element as HTMLElement;
|
||||
// 模拟菜单内容增多后尺寸变为 50x50
|
||||
Object.defineProperty(menuEl, 'clientHeight', { value: 50, configurable: true });
|
||||
Object.defineProperty(menuEl, 'clientWidth', { value: 50, configurable: true });
|
||||
|
||||
roCallbacks.forEach((cb) => cb());
|
||||
await nextTick();
|
||||
|
||||
expect((wrapper.vm as any).menuPosition.top).toBe(50);
|
||||
expect((wrapper.vm as any).menuPosition.left).toBe(50);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('菜单未显示时尺寸变化不修正位置', async () => {
|
||||
const wrapper = mount(ContentMenu as any, {
|
||||
...provideServices(),
|
||||
props: { menuData: [{ id: '1', type: 'button', text: 'a' }] as any },
|
||||
});
|
||||
Object.defineProperty(document.body, 'clientHeight', { value: 100, configurable: true });
|
||||
|
||||
roCallbacks.forEach((cb) => cb());
|
||||
await nextTick();
|
||||
|
||||
expect((wrapper.vm as any).menuPosition.top).toBe(0);
|
||||
expect((wrapper.vm as any).menuPosition.left).toBe(0);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('卸载时断开 ResizeObserver', () => {
|
||||
const wrapper = mount(ContentMenu as any, {
|
||||
...provideServices(),
|
||||
props: { menuData: [] as any },
|
||||
});
|
||||
wrapper.unmount();
|
||||
expect(FakeResizeObserver.disconnectSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('contains 判断 DOM 是否在菜单内部', async () => {
|
||||
const wrapper = mount(ContentMenu as any, {
|
||||
...provideServices(),
|
||||
|
||||
@ -7,8 +7,6 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { defineComponent, h } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import { FORM_SILENT_MODE_KEY } from '@tmagic/form';
|
||||
|
||||
import Code from '@editor/fields/Code.vue';
|
||||
|
||||
// 用一个简单的桩组件代替 MagicCodeEditor,把所有 props 原样渲染到 data-* 属性上,
|
||||
@ -47,7 +45,7 @@ vi.mock('@editor/layouts/CodeEditor.vue', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const mountCode = (props: Record<string, any>, global?: Record<string, any>) =>
|
||||
const mountCode = (props: Record<string, any>) =>
|
||||
mount(Code, {
|
||||
props: {
|
||||
// FieldProps 必填字段,用 as any 绕过测试中类型严格匹配
|
||||
@ -57,7 +55,6 @@ const mountCode = (props: Record<string, any>, global?: Record<string, any>) =>
|
||||
prop: 'codeField',
|
||||
...props,
|
||||
} as any,
|
||||
global,
|
||||
});
|
||||
|
||||
const getEl = (wrapper: ReturnType<typeof mountCode>) => wrapper.find('.fake-code-editor').element as HTMLElement;
|
||||
@ -171,28 +168,6 @@ describe('Code', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('静默模式', () => {
|
||||
test('注入 FORM_SILENT_MODE_KEY=true 时不渲染 CodeEditor', () => {
|
||||
const wrapper = mountCode(
|
||||
{},
|
||||
{
|
||||
provide: { [FORM_SILENT_MODE_KEY as symbol]: true },
|
||||
},
|
||||
);
|
||||
expect(wrapper.find('.fake-code-editor').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('注入 FORM_SILENT_MODE_KEY=false 时正常渲染 CodeEditor', () => {
|
||||
const wrapper = mountCode(
|
||||
{},
|
||||
{
|
||||
provide: { [FORM_SILENT_MODE_KEY as symbol]: false },
|
||||
},
|
||||
);
|
||||
expect(wrapper.find('.fake-code-editor').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('diff 模式 (对比)', () => {
|
||||
test('isCompare=true 且有 lastValues 时切换为 diff 模式', () => {
|
||||
const wrapper = mountCode({
|
||||
|
||||
@ -44,12 +44,6 @@ vi.mock('@tmagic/design', () => ({
|
||||
return () => h('div', { class: 'fake-card' }, slots.default?.());
|
||||
},
|
||||
}),
|
||||
TMagicButton: defineComponent({
|
||||
name: 'TMagicButton',
|
||||
setup(_p, { slots }) {
|
||||
return () => h('button', { class: 'fake-button' }, slots.default?.());
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const baseProps = (extra: any = {}) => ({
|
||||
@ -114,11 +108,13 @@ describe('CodeSelect', () => {
|
||||
expect((props.model.cs as any).hookData).toEqual([]);
|
||||
});
|
||||
|
||||
test('codeType items 配置正确', () => {
|
||||
test('codeType row items 配置正确', () => {
|
||||
const wrapper = mount(CodeSelect, { props: baseProps() as any });
|
||||
const container = wrapper.findComponent({ name: 'MContainer' });
|
||||
const config = container.props('config') as any;
|
||||
const codeTypeSelect = config.items[0];
|
||||
const row = config.items[0];
|
||||
expect(row.type).toBe('row');
|
||||
const codeTypeSelect = row.items[0];
|
||||
expect(codeTypeSelect.name).toBe('codeType');
|
||||
const setModel = vi.fn();
|
||||
codeTypeSelect.onChange(undefined, 'data-source-method', { setModel });
|
||||
@ -132,8 +128,9 @@ describe('CodeSelect', () => {
|
||||
const wrapper = mount(CodeSelect, { props: baseProps() as any });
|
||||
const container = wrapper.findComponent({ name: 'MContainer' });
|
||||
const config = container.props('config') as any;
|
||||
const codeIdCol = config.items[1];
|
||||
const dsCol = config.items[2];
|
||||
const row = config.items[0];
|
||||
const codeIdCol = row.items[1];
|
||||
const dsCol = row.items[2];
|
||||
expect(codeIdCol.display(undefined, { model: { codeType: 'code' } })).toBe(true);
|
||||
expect(codeIdCol.display(undefined, { model: { codeType: 'data-source-method' } })).toBe(false);
|
||||
expect(dsCol.display(undefined, { model: { codeType: 'data-source-method' } })).toBe(true);
|
||||
@ -144,8 +141,9 @@ describe('CodeSelect', () => {
|
||||
const wrapper = mount(CodeSelect, { props: baseProps() as any });
|
||||
const container = wrapper.findComponent({ name: 'MContainer' });
|
||||
const config = container.props('config') as any;
|
||||
const codeIdCol = config.items[1];
|
||||
const dsCol = config.items[2];
|
||||
const row = config.items[0];
|
||||
const codeIdCol = row.items[1];
|
||||
const dsCol = row.items[2];
|
||||
expect(codeIdCol.type).toBe('code-select-col');
|
||||
expect(codeIdCol.rules).toEqual([{ typeMatch: true, trigger: 'change' }]);
|
||||
expect(dsCol.type).toBe('data-source-method-select');
|
||||
@ -158,8 +156,9 @@ describe('CodeSelect', () => {
|
||||
const wrapper = mount(CodeSelect, { props: baseProps() as any });
|
||||
const container = wrapper.findComponent({ name: 'MContainer' });
|
||||
const config = container.props('config') as any;
|
||||
expect(config.items[1].notEditable()).toBe(true);
|
||||
expect(config.items[2].notEditable()).toBe(true);
|
||||
const row = config.items[0];
|
||||
expect(row.items[1].notEditable()).toBe(true);
|
||||
expect(row.items[2].notEditable()).toBe(true);
|
||||
codeBlockService.getEditStatus.mockReturnValue(true);
|
||||
dataSourceService.get.mockReturnValue(true);
|
||||
});
|
||||
|
||||
@ -526,11 +526,9 @@ describe('EventSelect', () => {
|
||||
],
|
||||
};
|
||||
const wrapper = mount(EventSelect, { props: baseProps({ model: m }) as any });
|
||||
// 用 class 选择器直击 panel header 里的删除按钮:模板里同时存在顶部 / 底部「添加事件」按钮,
|
||||
// 早期靠 `buttons[length - 1]` 取最后一个会误选到底部添加按钮,导致 events 没被删减。
|
||||
const deleteBtns = wrapper.findAll('.event-item-delete-button');
|
||||
expect(deleteBtns.length).toBe(m.events.length);
|
||||
await deleteBtns[deleteBtns.length - 1].trigger('click');
|
||||
const buttons = wrapper.findAll('button');
|
||||
const deleteBtn = buttons[buttons.length - 1];
|
||||
await deleteBtn.trigger('click');
|
||||
expect(m.events.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -91,7 +91,7 @@ const mkServices = () => {
|
||||
clear: vi.fn(),
|
||||
clearTargets: vi.fn(),
|
||||
clearIdleTasks: vi.fn(),
|
||||
collectIdle: vi.fn(async () => true),
|
||||
collectIdle: vi.fn(async () => undefined),
|
||||
collectByWorker: vi.fn(async () => undefined),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
|
||||
@ -95,7 +95,7 @@ vi.mock('@editor/layouts/props-panel/FormPanel.vue', () => ({
|
||||
// 模拟 CodeEditor 源码保存:仅传 values,无 eventData、无 error(对应 saveCode 路径)
|
||||
h('button', {
|
||||
class: 'code-save-btn',
|
||||
onClick: () => emit('submit', { id: 'n1', style: { color: 'red', width: '' } }),
|
||||
onClick: () => emit('submit', { id: 'n1', style: { color: 'red' } }),
|
||||
}),
|
||||
h('button', { class: 'submit-err-btn', onClick: () => emit('submit-error', new Error('e')) }),
|
||||
h('button', { class: 'form-err-btn', onClick: () => emit('form-error', new Error('e')) }),
|
||||
@ -239,16 +239,6 @@ describe('PropsPanel', () => {
|
||||
expect(options.historySource).toBe('code');
|
||||
});
|
||||
|
||||
test('CodeEditor 源码保存时 style 中的空字符串值被保留(表示清除该样式)', async () => {
|
||||
const wrapper = mount(PropsPanel, { props: {} as any });
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
await wrapper.find('.code-save-btn').trigger('click');
|
||||
|
||||
const calledNode = (editorService.update.mock.calls[0] as any)[0];
|
||||
expect(calledNode.style.color).toBe('red');
|
||||
expect(calledNode.style.width).toBe('');
|
||||
});
|
||||
|
||||
test('mounted 事件 emit', async () => {
|
||||
const wrapper = mount(PropsPanel, { props: {} as any });
|
||||
await wrapper.find('.mounted-btn').trigger('click');
|
||||
|
||||
@ -13,17 +13,16 @@ vi.mock('@editor/utils/dep/worker.ts?worker&inline', () => ({
|
||||
default: class FakeWorker {
|
||||
public static nextData: Record<string, any> = {};
|
||||
public static nextError = false;
|
||||
public static nextDelay = 0;
|
||||
public onmessage: ((e: any) => void) | null = null;
|
||||
public onerror: (() => void) | null = null;
|
||||
public postMessage() {
|
||||
setTimeout(() => {
|
||||
if (FakeWorker.nextError) {
|
||||
this.onerror?.();
|
||||
this.onerror?.(new Event('error'));
|
||||
return;
|
||||
}
|
||||
this.onmessage?.({ data: FakeWorker.nextData });
|
||||
}, FakeWorker.nextDelay);
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
}));
|
||||
@ -122,7 +121,7 @@ describe('Dep service', () => {
|
||||
test('collectIdle - 没有命中时立即 resolve 并 emit collected', async () => {
|
||||
const fn = vi.fn();
|
||||
depService.on('collected', fn);
|
||||
await expect(depService.collectIdle([{ id: 'n1', type: 'text' }] as any)).resolves.toBe(true);
|
||||
await depService.collectIdle([{ id: 'n1', type: 'text' }] as any);
|
||||
expect(fn).toHaveBeenCalled();
|
||||
depService.off('collected', fn);
|
||||
});
|
||||
@ -220,83 +219,6 @@ describe('Dep service', () => {
|
||||
fakeWorker.nextData = {};
|
||||
});
|
||||
|
||||
test('collectIdle 命中 target 时最终 resolve 并按批次 emit collected/ds-collected', async () => {
|
||||
depService.addTarget(makeTarget('ds1', DepTargetType.DATA_SOURCE));
|
||||
const collected = vi.fn();
|
||||
const dsCollected = vi.fn();
|
||||
depService.on('collected', collected);
|
||||
depService.on('ds-collected', dsCollected);
|
||||
|
||||
const nodes = [{ id: 'n1', type: 'text' }] as any;
|
||||
await expect(depService.collectIdle(nodes, {}, false, DepTargetType.DATA_SOURCE)).resolves.toBe(true);
|
||||
|
||||
expect(dsCollected).toHaveBeenCalledWith(nodes, false);
|
||||
expect(collected).toHaveBeenCalledWith(nodes, false);
|
||||
expect(depService.get('collecting')).toBe(false);
|
||||
|
||||
depService.off('collected', collected);
|
||||
depService.off('ds-collected', dsCollected);
|
||||
});
|
||||
|
||||
test('clearIdleTasks 会结算在途 collectIdle,避免 Promise 永久挂起且 collecting 复位', async () => {
|
||||
depService.addTarget(makeTarget('ds1', DepTargetType.DATA_SOURCE));
|
||||
|
||||
const promise = depService.collectIdle([{ id: 'n1', type: 'text' }] as any, {}, false, DepTargetType.DATA_SOURCE);
|
||||
expect(depService.get('collecting')).toBe(true);
|
||||
|
||||
// 快速触发:任务尚未执行就清空队列,批次应被主动结算而不是永久挂起
|
||||
depService.clearIdleTasks();
|
||||
|
||||
await expect(promise).resolves.toBe(false);
|
||||
expect(depService.get('collecting')).toBe(false);
|
||||
});
|
||||
|
||||
test('reset 会结算在途 collectIdle', async () => {
|
||||
depService.addTarget(makeTarget('ds1', DepTargetType.DATA_SOURCE));
|
||||
|
||||
const promise = depService.collectIdle([{ id: 'n1', type: 'text' }] as any, {}, false, DepTargetType.DATA_SOURCE);
|
||||
depService.reset();
|
||||
|
||||
await expect(promise).resolves.toBe(false);
|
||||
expect(depService.get('collecting')).toBe(false);
|
||||
});
|
||||
|
||||
test('reset 会忽略在途 worker 的过期结果,避免覆盖新依赖', async () => {
|
||||
const fakeWorker = (await import('@editor/utils/dep/worker.ts?worker&inline')).default as any;
|
||||
fakeWorker.nextDelay = 20;
|
||||
fakeWorker.nextData = {
|
||||
[DepTargetType.DATA_SOURCE]: { ds1: { n1: { data: {} } } },
|
||||
};
|
||||
|
||||
const workerPromise = depService.collectByWorker({ items: [], id: 'app', type: 'app' } as any);
|
||||
depService.reset();
|
||||
|
||||
const target = makeTarget('ds1', DepTargetType.DATA_SOURCE);
|
||||
depService.addTarget(target);
|
||||
const idlePromise = depService.collectIdle(
|
||||
[{ id: 'n1', type: 'text' }] as any,
|
||||
{},
|
||||
false,
|
||||
DepTargetType.DATA_SOURCE,
|
||||
);
|
||||
|
||||
await Promise.all([workerPromise, idlePromise]);
|
||||
expect(target.deps.n1).toBeUndefined();
|
||||
|
||||
fakeWorker.nextDelay = 0;
|
||||
fakeWorker.nextData = {};
|
||||
});
|
||||
|
||||
test('多个批次并发时各自独立 resolve,全部完成后 collecting 复位', async () => {
|
||||
depService.addTarget(makeTarget('ds1', DepTargetType.DATA_SOURCE));
|
||||
|
||||
const p1 = depService.collectIdle([{ id: 'n1', type: 'text' }] as any, {}, false, DepTargetType.DATA_SOURCE);
|
||||
const p2 = depService.collectIdle([{ id: 'n2', type: 'text' }] as any, {}, false, DepTargetType.DATA_SOURCE);
|
||||
|
||||
await Promise.all([p1, p2]);
|
||||
expect(depService.get('collecting')).toBe(false);
|
||||
});
|
||||
|
||||
test('destroy 会 reset 并移除监听', () => {
|
||||
depService.addTarget(makeTarget('destroy-me'));
|
||||
expect(() => depService.destroy()).not.toThrow();
|
||||
|
||||
@ -857,127 +857,6 @@ describe('change 事件', () => {
|
||||
expect(event.data[0].page?.id).toBe(NodeId.PAGE_ID);
|
||||
}
|
||||
});
|
||||
|
||||
test('change 事件携带历史来源 historySource,未传时为 undefined', async () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
const page = editorService.get('page') as unknown as MContainer;
|
||||
|
||||
const fn = vi.fn();
|
||||
editorService.on('change', fn);
|
||||
|
||||
// update
|
||||
await editorService.update({ id: NodeId.NODE_ID, type: 'text', text: 'source' }, { historySource: 'props' });
|
||||
expect(lastChangeEvent(fn).historySource).toBe('props');
|
||||
|
||||
// moveLayer
|
||||
await editorService.select(NodeId.NODE_ID);
|
||||
await editorService.moveLayer(1, { historySource: 'stage' });
|
||||
expect(lastChangeEvent(fn).historySource).toBe('stage');
|
||||
|
||||
// dragTo
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
await editorService.dragTo({ id: NodeId.NODE_ID2, type: 'text' }, page, 0, { historySource: 'stage' });
|
||||
expect(lastChangeEvent(fn).historySource).toBe('stage');
|
||||
|
||||
// add
|
||||
const newNode = (await editorService.add({ type: 'text' }, undefined, {
|
||||
historySource: 'component-panel',
|
||||
})) as MNode;
|
||||
expect(lastChangeEvent(fn).historySource).toBe('component-panel');
|
||||
|
||||
// remove
|
||||
await editorService.remove(newNode, { historySource: 'shortcut' });
|
||||
expect(lastChangeEvent(fn).historySource).toBe('shortcut');
|
||||
|
||||
// 未传 historySource 时为 undefined
|
||||
await editorService.update({ id: NodeId.NODE_ID, type: 'text', text: 'no-source' });
|
||||
expect(lastChangeEvent(fn).historySource).toBeUndefined();
|
||||
|
||||
// doNotPushHistory 透传:true 表示本次变更未写入历史记录,缺省为 false
|
||||
await editorService.update({ id: NodeId.NODE_ID, type: 'text', text: 'no-history' }, { doNotPushHistory: true });
|
||||
expect(lastChangeEvent(fn).doNotPushHistory).toBe(true);
|
||||
await editorService.update({ id: NodeId.NODE_ID, type: 'text', text: 'with-history' });
|
||||
expect(lastChangeEvent(fn).doNotPushHistory).toBe(false);
|
||||
|
||||
editorService.off('change', fn);
|
||||
});
|
||||
|
||||
test('撤销/重做触发的 change 事件携带被应用 step 的 source', async () => {
|
||||
historyService.reset();
|
||||
editorService.set('root', cloneDeep(root));
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
|
||||
await editorService.update({ id: NodeId.NODE_ID, type: 'text', text: 'undo-source' }, { historySource: 'props' });
|
||||
|
||||
const fn = vi.fn();
|
||||
editorService.on('change', fn);
|
||||
|
||||
await editorService.undo();
|
||||
expect(lastChangeEvent(fn).historySource).toBe('props');
|
||||
|
||||
await editorService.redo();
|
||||
expect(lastChangeEvent(fn).historySource).toBe('props');
|
||||
|
||||
editorService.off('change', fn);
|
||||
});
|
||||
|
||||
test('撤销 remove:重新插回节点时触发 change(type 为 add,携带 step source)', async () => {
|
||||
historyService.reset();
|
||||
editorService.set('root', cloneDeep(root));
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
|
||||
await editorService.remove({ id: NodeId.NODE_ID, type: 'text' }, { historySource: 'shortcut' });
|
||||
expect(editorService.getNodeById(NodeId.NODE_ID)).toBeNull();
|
||||
|
||||
const fn = vi.fn();
|
||||
editorService.on('change', fn);
|
||||
await editorService.undo();
|
||||
editorService.off('change', fn);
|
||||
|
||||
// 节点已按 step 记录的位置插回
|
||||
expect(editorService.getNodeById(NodeId.NODE_ID)?.id).toBe(NodeId.NODE_ID);
|
||||
const events = fn.mock.calls.map((call) => call[0]) as EditorChangeEvent[];
|
||||
const addEvent = events.find((e) => e.type === 'add');
|
||||
expect(addEvent).toBeTruthy();
|
||||
expect(addEvent?.historySource).toBe('shortcut');
|
||||
// 撤销/重做补发的事件不写入历史记录
|
||||
expect(addEvent?.doNotPushHistory).toBe(true);
|
||||
if (addEvent?.type === 'add') {
|
||||
expect(addEvent.data[0].node.id).toBe(NodeId.NODE_ID);
|
||||
expect(addEvent.data[0].page?.id).toBe(NodeId.PAGE_ID);
|
||||
}
|
||||
});
|
||||
|
||||
test('重做 add:重新插入节点时触发 change(type 为 add,携带 step source)', async () => {
|
||||
historyService.reset();
|
||||
editorService.set('root', cloneDeep(root));
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
|
||||
const newNode = (await editorService.add({ type: 'text' }, undefined, {
|
||||
historySource: 'component-panel',
|
||||
})) as MNode;
|
||||
// 撤销 add,使节点从 DSL 中移除
|
||||
await editorService.undo();
|
||||
expect(editorService.getNodeById(newNode.id)).toBeNull();
|
||||
|
||||
const fn = vi.fn();
|
||||
editorService.on('change', fn);
|
||||
await editorService.redo();
|
||||
editorService.off('change', fn);
|
||||
|
||||
// 节点已重新插入
|
||||
expect(editorService.getNodeById(newNode.id)?.id).toBe(newNode.id);
|
||||
const events = fn.mock.calls.map((call) => call[0]) as EditorChangeEvent[];
|
||||
const addEvent = events.find((e) => e.type === 'add');
|
||||
expect(addEvent).toBeTruthy();
|
||||
expect(addEvent?.historySource).toBe('component-panel');
|
||||
// 撤销/重做补发的事件不写入历史记录
|
||||
expect(addEvent?.doNotPushHistory).toBe(true);
|
||||
if (addEvent?.type === 'add') {
|
||||
expect(addEvent.data[0].node.id).toBe(newNode.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('undo redo', () => {
|
||||
|
||||
@ -105,8 +105,8 @@ describe('data-source utils', () => {
|
||||
fields: [],
|
||||
});
|
||||
expect(resolveFieldByPath(fields, ['obj']).field?.name).toBe('obj');
|
||||
expect(resolveFieldByPath(fields, ['unknown'])).toEqual({ ok: false, fields, failedName: 'unknown' });
|
||||
expect(resolveFieldByPath(undefined, ['x'])).toEqual({ ok: false, fields: [], failedName: 'x' });
|
||||
expect(resolveFieldByPath(fields, ['unknown'])).toEqual({ ok: false, fields });
|
||||
expect(resolveFieldByPath(undefined, ['x'])).toEqual({ ok: false, fields: [] });
|
||||
expect(resolveFieldByPath(fields, []).ok).toBe(true);
|
||||
});
|
||||
|
||||
@ -124,9 +124,7 @@ describe('data-source utils', () => {
|
||||
expect(result.field?.name).toBe('item');
|
||||
expect(result.fields).toEqual([{ name: 'n', type: 'string' }]);
|
||||
|
||||
const failed = resolveFieldByPath(fields, ['arr', '0', 'missing'], { skipNumberIndices: true });
|
||||
expect(failed.ok).toBe(false);
|
||||
expect(failed.failedName).toBe('missing');
|
||||
expect(resolveFieldByPath(fields, ['arr', '0', 'missing'], { skipNumberIndices: true }).ok).toBe(false);
|
||||
});
|
||||
|
||||
test('getFieldType 沿 path 取最终类型', () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user