feat(design,editor): 新增magic-admin主题样式,优化组件表单配置布局样式

This commit is contained in:
manmanyu 2026-07-22 09:31:26 +00:00 committed by roymondchen
parent 89689bb9fd
commit b13cd3425b
114 changed files with 2025 additions and 373 deletions

View File

@ -162,7 +162,7 @@
}
&:hover {
color: #409eff;
color: var(--el-color-primary,#409eff);;
background-color: #f9fafc;
}

View File

@ -16,7 +16,7 @@ export default defineConfig([
'*/**/public/**/*',
'*/**/types/**/*',
'*/**/*.config.ts',
'./tepm/**/*',
'./temp/**/*',
'vite-env.d.ts',
]),
...eslintConfig(path.join(path.dirname(fileURLToPath(import.meta.url)), 'tsconfig.json')),

View File

@ -0,0 +1,23 @@
<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>

View File

@ -1,5 +1,5 @@
<template>
<component class="tmagic-design-card" :is="uiComponent" v-bind="uiProps">
<component :is="uiComponent" :class="['tmagic-design-card', { 'tmagic-design-card--flat': isFlat }]" v-bind="uiProps">
<template #header v-if="$slots.header">
<slot name="header" class="header"></slot>
</template>
@ -14,6 +14,7 @@
import { computed } from 'vue';
import { getDesignConfig } from './config';
import { isGlobalFlat } from './index';
import type { CardProps } from './types';
defineOptions({
@ -26,5 +27,14 @@ const ui = getDesignConfig('components')?.card;
const uiComponent = ui?.component || 'el-card';
const uiProps = computed<CardProps>(() => ui?.props(props) || props);
// `<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);
});
</script>

View File

@ -7,18 +7,39 @@
@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 } from 'vue';
import { computed, type ComputedRef, inject } 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,

View File

@ -1,12 +1,24 @@
<template>
<component class="tmagic-design-form-item" :is="uiComponent" v-bind="uiProps">
<component
class="tmagic-design-form-item"
:class="{ 'has-extra-tips': adapterType === 'element-plus' && extraTips }"
:is="uiComponent"
v-bind="uiProps"
>
<template #label>
<slot name="label"></slot>
<slot name="label"> </slot>
</template>
<template #default>
<slot></slot>
<div v-if="adapterType === 'element-plus' && extra" v-html="extra" class="m-form-tip"></div>
<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>
</template>
<template v-if="adapterType === 'element-plus'" #error="{ error }">
@ -16,26 +28,32 @@
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { computed, inject } 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>();
const props = defineProps<FormItemProps & { theme?: string }>();
const ui = getDesignConfig('components')?.formItem;
const uiComponent = ui?.component || 'el-form-item';
const adapterType = getDesignConfig('adapterType');
const formInline = inject<boolean>('formInline');
const uiProps = computed<FormItemProps>(() => {
const { extra, ...rest } = ui?.props(props) || props;
const { extra, extraTips, ...rest } = ui?.props(props) || props;
if (isGlobalFlat.value && rest.labelPosition === undefined) {
return { ...rest, labelPosition: formInline ? 'right' : 'left' };
}
return rest;
});

View File

@ -4,10 +4,9 @@
<div
v-if="popoverVisible || !destroyOnClose"
v-show="popoverVisible"
class="tmagic-design-popper"
ref="popperElementRef"
:tabindex="tabindex"
:class="popperClass"
:class="['tmagic-design-popper', popperClass, themeClass]"
:style="style"
@mouseenter.once="popperMouseenterHandler"
>
@ -22,7 +21,7 @@ import { computed, getCurrentInstance, nextTick, onBeforeUnmount, onMounted, ref
import type { Instance } from '@popperjs/core';
import { createPopper } from '@popperjs/core';
import { useZIndex } from './index';
import { useThemeClass, useZIndex } from './index';
import type { PopoverProps } from './types';
defineSlots<{
@ -46,6 +45,12 @@ 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];

View File

@ -8,6 +8,7 @@ import './theme/index.scss';
export * from './types';
export * from './config';
export * from './theme';
export * from './formValidateMessage';
export { default as TMagicAutocomplete } from './Autocomplete.vue';
@ -94,6 +95,10 @@ 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 =
@ -129,6 +134,8 @@ 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}`);
}

View File

@ -0,0 +1,59 @@
/*
* 穿 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 proxycomputed
* `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 || '');
};

View File

@ -0,0 +1,67 @@
// `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;
}
}
}

View File

@ -0,0 +1,4 @@
.tmagic-design-color-picker-input {
margin-left: 8px;
width: calc(var(--el-input-width, 100%) - 40px);
}

View File

@ -1 +1,3 @@
@use "./popover.scss";
@use "./card.scss";
@use "./colorPicker.scss";

View File

@ -0,0 +1,38 @@
@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;
}
}
}
}
}

View File

@ -24,6 +24,17 @@ 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;
@ -39,6 +50,11 @@ export interface CardProps {
bodyStyle?: Record<string, any>;
shadow?: string;
header?: string;
/**
* / / / / header 线 / header body
* 使TMagicCard prop 使
*/
flat?: boolean;
}
export interface CascaderProps {
@ -97,6 +113,7 @@ export interface ColorPickerProps {
disabled?: boolean;
showAlpha?: boolean;
size?: FieldSize;
flat?: boolean;
}
export interface DatePickerProps {
@ -185,7 +202,9 @@ export interface FormItemProps {
labelWidth?: string | number;
rules?: any;
extra?: string;
extraTips?: string;
labelPosition?: 'top' | 'left' | 'right';
text?: string;
}
export interface InputProps {
@ -464,6 +483,11 @@ 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;
@ -726,6 +750,7 @@ export interface Components {
}
export interface DesignPluginOptions {
flat?: boolean;
adapterType?: string;
message?: TMagicMessage;
messageBox?: TMagicMessageBox;

View File

@ -4,6 +4,7 @@
:page-bar-sort-options="pageBarSortOptions"
:page-filter-function="pageFilterFunction"
:hide-sidebar="hideSidebar"
:theme="theme"
>
<template #header>
<slot name="header"></slot>
@ -135,9 +136,10 @@
<script lang="ts" setup>
import { EventEmitter } from 'events';
import { provide, ref } from 'vue';
import { computed, 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';
@ -241,6 +243,15 @@ 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 上下文保持一致
@ -256,6 +267,16 @@ 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);

View File

@ -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 { useZIndex } from '@tmagic/design';
import { useThemeClass, useZIndex } from '@tmagic/design';
import { MenuButton, MenuComponent } from '@editor/type';
@ -73,6 +73,14 @@ 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);

View File

@ -2,7 +2,7 @@
<Teleport to="body" v-if="visible">
<div
ref="target"
class="m-editor-float-box"
:class="['m-editor-float-box', themeClass]"
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, useZIndex } from '@tmagic/design';
import { TMagicButton, useThemeClass, useZIndex } from '@tmagic/design';
import MIcon from '@editor/components/Icon.vue';
@ -62,6 +62,12 @@ 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);

View File

@ -35,6 +35,10 @@ import type {
export const ENABLE_PROPS_FORM_VALIDATE: InjectionKey<boolean> = Symbol('enablePropsFormValidate');
export interface EditorProps {
/** 是否是大屏模拟器容器大屏容器时左侧属性面板会扩展为两列正常3列颜色表单不扩展两列等 */
isLargeStageContainer?: boolean;
theme?: string;
/** 页面初始值 */
modelValue?: MApp;
/** 左侧面板中的组件类型列表 */

View File

@ -1,9 +1,10 @@
<template>
<div class="m-fields-code-select" :class="config.className">
<TMagicCard>
<TMagicCard :flat="config.flat">
<MContainer
:config="codeConfig"
:size="size"
class="code-select-content"
:prop="prop"
:disabled="disabled"
:is-compare="isCompareMode"
@ -12,16 +13,20 @@
@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 { TMagicCard } from '@tmagic/design';
import { TMagicButton, TMagicCard } from '@tmagic/design';
import type { CodeSelectConfig, ContainerChangeEventData, FieldProps, GroupListConfig } from '@tmagic/form';
import { MContainer } from '@tmagic/form';
@ -53,12 +58,24 @@ 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,
flat: true,
addable: () => false,
title: (mForm, { model, index }: any) => {
if (model.codeType === HookCodeType.DATA_SOURCE_METHOD) {
if (Array.isArray(model.codeId)) {
@ -81,49 +98,44 @@ const codeConfig = computed<GroupListConfig>(() => ({
return model.codeId || index;
},
titlePrefix: props.config.name === undefined ? undefined : String(props.config.name),
items: [
{
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', '');
}
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' }],
},
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' }],
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'),
},
],
}));

View File

@ -147,6 +147,7 @@ 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) => ({

View File

@ -1,7 +1,12 @@
<template>
<div class="m-fields-data-source-field-select">
<div
class="m-fields-data-source-field-select"
:class="{
[`data-source-field-${type}`]: !isSelectValid,
}"
>
<FieldSelect
v-if="!disabledDataSource && (showDataSourceFieldSelect || !config.fieldConfig)"
v-if="isSelectValid"
:model-value="model[name]"
:disabled="disabled"
:size="size"
@ -37,15 +42,14 @@
:size="size"
:disabled="disabled"
@click="onToggleDataSourceFieldSelectHandler"
><MIcon :icon="Coin"></MIcon
></TMagicButton>
><MIcon :icon="dataSourceIcon"></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';
@ -61,6 +65,8 @@ 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({
@ -75,6 +81,9 @@ 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) => {

View File

@ -68,12 +68,15 @@ const config = computed<GroupListConfig>(() => ({
titlePrefix: props.config.titlePrefix,
expandAll: true,
enableToggleMode: false,
flat: props.config.flat,
items: [
{
type: 'table',
name: 'cond',
operateColWidth: 80,
operateColWidth: props.config.operateColWidth,
enableToggleMode: false,
fixed: props.config.fixed,
flat: props.config.flat,
items: [
parentFields.value.length
? {

View File

@ -12,16 +12,21 @@
@change="onChangeHandler"
></MTable>
<div v-else class="fullWidth">
<TMagicButton
v-if="!isCompareMode"
class="create-button"
type="primary"
:size="size"
:disabled="disabled"
@click="addEvent()"
>添加事件</TMagicButton
>
<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>
<MPanel
v-for="entry in displayList"
:key="entry.index"
@ -32,32 +37,45 @@
:model="entry.cardItem"
:last-values="entry.lastCardItem"
:is-compare="isCompareMode"
:hide-expand="true"
:label-width="config.labelWidth || '100px'"
@change="onChangeHandler"
>
<template #header>
<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>
<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>
</template>
</MPanel>
<TMagicButton
v-if="!isCompareMode"
class="create-button fullWidth"
:icon="Plus"
:disabled="disabled"
@click="addEvent()"
>添加事件</TMagicButton
>
</div>
</div>
</template>
@ -65,6 +83,7 @@
<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';
@ -108,7 +127,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' ||
@ -118,7 +137,7 @@ const eventNameConfig = computed(() => {
}
return 'select';
},
labelWidth: '40px',
labelWidth: '70px',
checkStrictly: () => props.config.src !== 'component',
valueSeparator: '.',
options: (_mForm: FormState, { formValue }: any) => getEventNameOptions(props.config.src, formValue),
@ -178,6 +197,7 @@ const actionTypeConfig = computed(() => {
name: 'actionType',
text: '联动类型',
type: 'select',
labelPosition: 'left',
defaultValue: ActionType.COMP,
options: actionTypeOptions.value,
rules: [
@ -200,6 +220,7 @@ const targetCompConfig = computed(() => {
name: 'to',
text: '联动组件',
type: 'ui-select',
labelPosition: 'left',
display: (_mForm, { model }) => model.actionType === ActionType.COMP,
onChange: (_MForm, _v, { setModel }) => {
setModel('method', '');
@ -219,6 +240,7 @@ const compActionConfig = computed(() => {
const defaultCompActionConfig: DynamicTypeConfig = {
name: 'method',
text: '动作',
labelPosition: 'left',
type: (mForm: FormState | undefined, { model }: any) => {
const to = editorService.getNodeById(model.to);
@ -317,6 +339,8 @@ const actionsConfig = computed(
() =>
defineFormItem({
type: 'panel',
labelPosition: 'left',
flat: true,
items: [
{
type: 'group-list',
@ -324,6 +348,8 @@ const actionsConfig = computed(
expandAll: true,
enableToggleMode: false,
titlePrefix: '动作',
labelPosition: 'left',
flat: true,
items: [
actionTypeConfig.value,
targetCompConfig.value,

View File

@ -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><MIcon :icon="Grid"></MIcon>{{ item.title }}</template>
<template #title>{{ item.title }}</template>
<component
v-if="item.component"
:is="item.component"
@ -22,14 +22,11 @@
<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({

View File

@ -15,7 +15,7 @@
@click="selectDirection('Left')"
></div>
<div
class="border-icon"
class="border-icon center"
:class="{ active: direction === '', configured: isConfigured('') }"
@click="selectDirection()"
></div>

View File

@ -0,0 +1,19 @@
<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>

View File

@ -0,0 +1,19 @@
<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>

View File

@ -0,0 +1,19 @@
<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>

View File

@ -0,0 +1,34 @@
<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>

View File

@ -0,0 +1,34 @@
<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>

View File

@ -0,0 +1,5 @@
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';

View File

@ -1,8 +1,24 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" fill="currentColor">
<path
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>
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"
/>
</svg>
</template>

View File

@ -1,31 +1,13 @@
<template>
<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 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>
</template>

View File

@ -1,8 +1,7 @@
<template>
<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 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>
</template>

View File

@ -1,8 +1,7 @@
<template>
<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 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>
</template>

View File

@ -1,7 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path
d="M896 320H128V0h768v320z m0 320H128v-256h768v256z m-128 192l-256 192-256-192 192-0.032V704h128v128h192z"
></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" />
</svg>
</template>

View File

@ -1,7 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path
d="M896 704H128v320h768V704z m0-320H128v256h768v-256z m-128-192l-256-192-256 192 192 0.032V320h128V192h192z"
></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" />
</svg>
</template>

View File

@ -1,7 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path
d="M320 128v768H0V128h320z m320 0v768h-256V128h256z m192 128l192 256-192 256-0.032-192H704v-128h128V256z"
></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" />
</svg>
</template>

View File

@ -1,7 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path
d="M704 128v768h320V128H704zM384 128v768h256V128h-256zM192 256l-192 256 192 256 0.032-192H320v-128H192V256z"
></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" />
</svg>
</template>

View File

@ -1,5 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path d="M480 1024V0h64v1024h-64z m128-64V64h320v896H608zM96 960V64h320v896H96z"></path>
<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>
</template>

View File

@ -1,5 +1,18 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path d="M416 160H96v704h320V160z m384 0H480v704h320V160z m128-160h-64v1024h64V0z"></path>
<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>
</template>

View File

@ -1,5 +1,18 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path d="M608 160h320v704H608V160zM224 160h320v704H224V160zM96 0h64v1024H96V0z"></path>
<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>
</template>

View File

@ -1,7 +1,32 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path
d="M320 864H192v160H128v-160H0V160h128V0h64v160h128v704z m704 0h-128v160h-64v-160h-128V160h128V0h64v160h128v704z"
></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"
/>
</svg>
</template>

View File

@ -1,5 +1,31 @@
<template>
<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 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>
</template>

View File

@ -29,6 +29,7 @@ defineProps<{
isCompare?: boolean;
disabled?: boolean;
size?: 'large' | 'default' | 'small';
theme?: string;
prop?: string;
}>();
@ -112,10 +113,14 @@ const formConfig = defineFormConfig([
childType: 'button',
labelWidth: '68px',
options: [
{ value: 'repeat', icon: markRaw(BackgroundRepeat), tooltip: '垂直和水平方向重复 repeat' },
{ value: 'no-repeat', icon: markRaw(BackgroundNoRepeat), tooltip: '不重复 no-repeat' },
{ value: 'repeat-x', icon: markRaw(BackgroundRepeatX), tooltip: '水平方向重复 repeat-x' },
{ value: 'repeat-y', icon: markRaw(BackgroundRepeatY), tooltip: '垂直方向重复 repeat-y' },
{ value: 'no-repeat', icon: markRaw(BackgroundNoRepeat), tooltip: '不重复 no-repeat' },
{
value: 'repeat',
icon: markRaw(BackgroundRepeat),
tooltip: '垂直和水平方向重复 repeat',
},
],
},
{

View File

@ -95,9 +95,9 @@ const formConfig = defineFormConfig([
childType: 'button',
labelWidth: '68px',
options: [
{ value: 'left', icon: markRaw(AlignLeft), tooltip: '左对齐 row' },
{ value: 'center', icon: markRaw(AlignCenter), tooltip: '居中对齐 center' },
{ value: 'right', icon: markRaw(AlignRight), tooltip: '右对齐 right' },
{ value: 'left', icon: markRaw(AlignLeft), tooltip: '左对齐 row', text: '左对齐' },
{ value: 'center', icon: markRaw(AlignCenter), tooltip: '居中对齐 center', text: '居中对齐' },
{ value: 'right', icon: markRaw(AlignRight), tooltip: '右对齐 right', text: '右对齐' },
],
},
]);

View File

@ -26,11 +26,19 @@
<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,
@ -46,12 +54,13 @@ import {
JustifyContentSpaceBetween,
} from '../icons/justify-content';
defineProps<{
const props = defineProps<{
values: Partial<StyleSchema>;
lastValues?: Partial<StyleSchema>;
isCompare?: boolean;
disabled?: boolean;
size?: 'large' | 'default' | 'small';
theme?: string;
prop?: string;
}>();
@ -60,19 +69,42 @@ const emit = defineEmits<{
addDiffCount: [];
}>();
const displayTheme = useTheme(props);
const formConfig = defineFormConfig([
{
name: 'display',
text: '模式',
type: 'radioGroup',
childType: 'button',
labelWidth: '68px',
labelWidth: '90px',
iconSize: '24px',
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',
},
],
},
{
@ -80,11 +112,20 @@ const formConfig = defineFormConfig([
text: '主轴方向',
type: 'radioGroup',
childType: 'button',
labelWidth: '68px',
labelWidth: '90px',
iconSize: '24px',
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),
@ -98,13 +139,22 @@ const formConfig = defineFormConfig([
text: '主轴对齐',
type: 'radioGroup',
childType: 'button',
labelWidth: '68px',
labelWidth: '90px',
iconSize: '24px',
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',
},
@ -113,13 +163,22 @@ const formConfig = defineFormConfig([
text: '辅轴对齐',
type: 'radioGroup',
childType: 'button',
labelWidth: '68px',
labelWidth: '90px',
iconSize: '24px',
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: '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',
},
],
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
},
@ -127,8 +186,9 @@ const formConfig = defineFormConfig([
name: 'flexWrap',
text: '换行',
type: 'radioGroup',
childType: 'button',
labelWidth: '68px',
childType: displayTheme.value !== 'magic-admin' ? 'button' : 'default',
labelWidth: '90px',
iconSize: '24px',
options: [
{ value: 'nowrap', text: '不换行', tooltip: '不换行 nowrap' },
{ value: 'wrap', text: '正换行', tooltip: '第一行在上方 wrap' },
@ -141,17 +201,22 @@ const formConfig = defineFormConfig([
items: [
{
name: 'width',
text: '宽度',
labelWidth: '68px',
text: '宽度px',
labelWidth: '90px',
type: 'data-source-field-select',
fieldConfig: {
type: 'text',
},
},
],
},
{
type: 'row',
items: [
{
name: 'height',
text: '高度',
labelWidth: '68px',
text: '高度px',
labelWidth: '90px',
type: 'data-source-field-select',
fieldConfig: {
type: 'text',
@ -166,7 +231,7 @@ const formConfig = defineFormConfig([
type: 'data-source-field-select',
text: 'overflow',
name: 'overflow',
labelWidth: '68px',
labelWidth: '90px',
checkStrictly: false,
dataSourceFieldType: ['string'],
fieldConfig: {
@ -184,11 +249,16 @@ const formConfig = defineFormConfig([
],
},
},
],
},
{
type: 'row',
items: [
{
type: 'data-source-field-select',
text: '透明度',
text: '透明度%',
name: 'opacity',
labelWidth: '68px',
labelWidth: '90px',
dataSourceFieldType: ['string', 'number'],
fieldConfig: {
type: 'text',

File diff suppressed because one or more lines are too long

View File

@ -32,6 +32,9 @@ 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';

View File

@ -1,5 +1,9 @@
<template>
<div class="m-editor" ref="content" style="min-width: 900px">
<div
ref="content"
:class="['m-editor', theme ? `m-editor--${theme}` : '', theme ? `m-theme--${theme}` : '']"
style="min-width: 900px"
>
<slot name="header"></slot>
<slot name="nav"></slot>
@ -97,6 +101,8 @@ const props = defineProps<{
pageFilterFunction?: (_page: MPage | MPageFragment, _keyword: string) => boolean;
/** 是否隐藏左侧面板 */
hideSidebar?: boolean;
/** 主题名称,会在根节点追加 `m-editor--<theme>` 修饰类 */
theme?: string;
}>();
const codeOptions = inject('codeOptions', {});

View File

@ -111,14 +111,31 @@ 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');
curFormConfig.value = await propsService.getPropsConfig(type, { node: node.value });
const config = await propsService.getPropsConfig(type, { node: node.value });
// init /
if (seq !== initSeq || !mounted) return;
if (!node.value) return;
curFormConfig.value = config;
values.value = node.value;
};
@ -126,6 +143,7 @@ watchEffect(init);
propsService.on('props-configs-change', init);
onBeforeUnmount(() => {
mounted = false;
propsService.off('props-configs-change', init);
});

View File

@ -155,7 +155,7 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { Close, Coin, EditPen, Goods, List } from '@element-plus/icons-vue';
import { Box, Close, Coin, EditPen, Finished } 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: Goods,
icon: Box,
text: '组件',
component: ComponentListPanel,
slots: {},
@ -261,7 +261,7 @@ const getItemConfig = (data: SideItem): SideComponent => {
layer: {
$key: SideItemKey.LAYER,
type: 'component',
icon: List,
icon: Finished,
text: '已选组件',
props: {
layerContentMenu: props.layerContentMenu,

View File

@ -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,6 +56,8 @@ 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 {
@ -69,7 +71,6 @@ 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);

View File

@ -49,13 +49,14 @@ const state = shallowReactive<UiState>({
width: 375,
height: 817,
},
// 先给 columnWidth 留占位,真正的 clamp 逻辑在 state 创建完成后
// 用 state.minLeftColumnWidth / state.minRightColumnWidth 作为下限重新赋值 —
// 这样以后从外部(比如 uiService.set('minLeftColumnWidth', ...))动态改最小宽度,
// 只改一处 state 字段即可,不用同步 MIN_* 常量。
columnWidth: {
left:
storageService.getItem(LEFT_COLUMN_WIDTH_STORAGE_KEY, { protocol: Protocol.NUMBER }) || DEFAULT_LEFT_COLUMN_WIDTH,
left: 0,
center: 0,
right:
storageService.getItem(RIGHT_COLUMN_WIDTH_STORAGE_KEY, { protocol: Protocol.NUMBER }) ||
DEFAULT_RIGHT_COLUMN_WIDTH,
right: 0,
},
minLeftColumnWidth: MIN_LEFT_COLUMN_WIDTH,
minCenterColumnWidth: MIN_CENTER_COLUMN_WIDTH,
@ -83,6 +84,20 @@ 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,

View File

@ -1,3 +1,40 @@
.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;
}
}
}

View File

@ -1,4 +1,4 @@
$theme-color: #2882e0;
$theme-color: var(--el-color-primary, #2882e0);
$font-color: #313a40;
$border-color: #d9dbdd;

View File

@ -77,9 +77,9 @@
margin-bottom: 5px;
&:hover {
background: #2882e0;
background: var(--el-color-primary, #2882e0);
color: #fff;
border-color: #4e8be1;
border-color: var(--el-color-primary, #4e8be1);
}
}

View File

@ -5,6 +5,10 @@
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;

View File

@ -0,0 +1,11 @@
.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;
}
}

View File

@ -2,8 +2,29 @@
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;
@ -36,10 +57,43 @@
}
}
.el-form-item {
.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 {
&.is-error {
margin-bottom: 18px;
.el-form-item {
&.is-error {
margin-bottom: 18px;
}
}
}
}

View File

@ -6,7 +6,10 @@
width: 100%;
&-content {
height: calc(100% - #{$nav-height});
// nav-menu.scss `--editor-nav-height` 联动默认 fallback SCSS
// `$nav-height`35px主题覆盖时两处会同时切成同一个高度保证 nav
// 主内容区总高度 = 100%
height: calc(100% - var(--editor-nav-height, #{$nav-height}));
}
&-framework-center {

View File

@ -21,7 +21,7 @@
color: #909399;
&:hover {
color: #409eff;
color: var(--el-color-primary, #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 #409eff;
box-shadow: inset 2px 0 0 var(--el-color-primary, #409eff);
&:hover {
background-color: rgba(64, 158, 255, 0.16);
@ -104,7 +104,7 @@
.m-editor-history-list-item-desc {
font-weight: 600;
color: #409eff;
color: var(--el-color-primary, #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: #409eff;
border-left-color: var(--el-color-primary, #409eff);
box-shadow: none; // 覆盖 .is-current 公共的 inset 阴影
.m-editor-history-list-group-head {
color: #409eff;
color: var(--el-color-primary, #409eff);
}
}
}
@ -216,7 +216,7 @@
}
&.is-current {
color: #409eff;
color: var(--el-color-primary, #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: #409eff;
background-color: var(--el-color-primary, #409eff);
font-weight: 500;
}
@ -395,7 +395,7 @@
border-radius: 2px;
font-size: 10px;
line-height: 16px;
color: #409eff;
color: var(--el-color-primary, #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: #409eff;
color: var(--el-color-primary, #409eff);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

View File

@ -13,7 +13,11 @@
font-weight: 400;
box-sizing: border-box;
margin: 0px;
flex: 0 0 $nav-height;
// CSS 变量做主题化开关默认没设 `--editor-nav-height` 时回落到
// SCSS 变量 `$nav-height`common/var.scss35px主题 magic-admin
// 只需在祖先节点声明 `--editor-nav-height: 40px` 即可运行时覆盖避免
// `@use with` 那条对加载顺序极敏感的 SCSS 模块配置路径
flex: 0 0 var(--editor-nav-height, #{$nav-height});
border-bottom: 1px solid #d8dee8;
> div {

View File

@ -129,7 +129,8 @@
> .m-container-tab {
> .tmagic-design-tabs {
> .el-tabs__content {
padding-top: 55px;
margin-top: var(--el-tabs-header-height);
padding-top: 15px;
}
> .el-tabs__header.is-top {
position: absolute;

View File

@ -1,5 +1,6 @@
.border-box-container {
display: flex;
--unuse-border-style: dashed;
.border-icon-container {
display: flex;
flex-direction: column;
@ -28,24 +29,32 @@
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 dashed dashed dashed;
border-style: solid var(--unuse-border-style) var(--unuse-border-style)
var(--unuse-border-style);
}
&.border-icon-right {
border-right-width: 2px;
border-style: dashed solid dashed dashed;
border-style: var(--unuse-border-style) solid var(--unuse-border-style)
var(--unuse-border-style);
}
&.border-icon-bottom {
border-bottom-width: 2px;
border-style: dashed dashed solid dashed;
border-style: var(--unuse-border-style) var(--unuse-border-style) solid
var(--unuse-border-style);
}
&.border-icon-left {
border-left-width: 2px;
border-style: dashed dashed dashed solid;
border-style: var(--unuse-border-style) var(--unuse-border-style)
var(--unuse-border-style) solid;
}
}
}

View File

@ -27,6 +27,7 @@
@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";

View File

@ -0,0 +1,214 @@
// 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:5240 是老 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;
}
}
}
}
}

View File

@ -4,6 +4,6 @@
margin-right: 3px;
}
span {
color: #2882e0;
color: var(--el-color-primary, #2882e0);
}
}

View File

@ -140,6 +140,7 @@ 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'),

View File

@ -16,14 +16,22 @@
* 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, setEditorConfig };
export { getEditorConfig, isGlobalFlat, setEditorConfig };

View File

@ -5,10 +5,11 @@ 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 = 310;
export const DEFAULT_LEFT_COLUMN_WIDTH = 345;
export const DEFAULT_RIGHT_COLUMN_WIDTH = 480;
export const MIN_LEFT_COLUMN_WIDTH = 200;
// 2026-06-11 设计稿修改,固定左侧栏存在时最小宽度为 345
export const MIN_LEFT_COLUMN_WIDTH = 345;
export const MIN_CENTER_COLUMN_WIDTH = 400;
export const MIN_RIGHT_COLUMN_WIDTH = 300;

View File

@ -180,6 +180,7 @@ export const advancedTabConfig: TabPaneConfig = {
name: NODE_DISABLE_CODE_BLOCK_KEY,
text: '禁用代码块',
type: 'switch',
labelPosition: 'left',
defaultValue: false,
extra: '开启后,配置的代码块将不会被执行',
},
@ -187,15 +188,17 @@ export const advancedTabConfig: TabPaneConfig = {
name: NODE_DISABLE_DATA_SOURCE_KEY,
text: '禁用数据源',
type: 'switch',
labelPosition: 'left',
defaultValue: false,
extra: '开启后,组件内配置的数据源相关配置将不会被编译,显隐条件将失效',
},
{
name: 'created',
text: 'created',
text: 'Created',
flat: true,
labelPosition: 'top',
type: 'code-select',
extra: '组件初始化时执行',
titleExtra: '组件初始化时执行',
rules: [
{ typeMatch: true, trigger: 'change' },
{
@ -210,10 +213,12 @@ export const advancedTabConfig: TabPaneConfig = {
},
{
name: 'mounted',
text: 'mounted',
text: 'Mounted',
labelPosition: 'top',
flat: true,
type: 'code-select',
extra: '组件挂载到dom时执行',
titleExtra: '组件挂载到dom时执行',
rules: [
{ typeMatch: true, trigger: 'change' },
{
@ -228,8 +233,9 @@ export const advancedTabConfig: TabPaneConfig = {
},
{
name: 'display',
text: 'display',
extra: '控制组件是否渲染关系的代码块返回值为false时不渲染',
text: 'Display',
flat: true,
titleExtra: '控制组件是否渲染关系的代码块返回值为false时不渲染',
labelPosition: 'top',
type: 'code-select',
rules: [
@ -263,10 +269,18 @@ 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: '条件组',
flat: true,
fixed: 'right',
operateColWidth: 112,
defaultValue: [],
rules: [{ typeMatch: true }],
},
@ -303,10 +317,13 @@ export const fillConfig = (
name: 'id',
text: 'ID',
type: 'text',
disabled: true,
// 走纯文本渲染,避免出现 disabled 的灰底输入框append 仍正常显示。
static: true,
append: {
type: 'button',
text: '复制',
type: 'icon',
text: 'https://vip.image.video.qpic.cn/vupload/20260615/36cf7e1781493669935.svg',
extra: '复制',
handler: (vm, { model }) => {
navigator.clipboard
.writeText(`${model.id}`)
@ -339,6 +356,7 @@ export const fillConfig = (
const tabConfig: TabConfig = {
type: 'tab',
labelWidth,
className: 'magic-right-panel-tabs-top',
items: [
{
title: '属性',

View File

@ -44,6 +44,12 @@ 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 = {}) => ({
@ -108,13 +114,11 @@ describe('CodeSelect', () => {
expect((props.model.cs as any).hookData).toEqual([]);
});
test('codeType row items 配置正确', () => {
test('codeType items 配置正确', () => {
const wrapper = mount(CodeSelect, { props: baseProps() as any });
const container = wrapper.findComponent({ name: 'MContainer' });
const config = container.props('config') as any;
const row = config.items[0];
expect(row.type).toBe('row');
const codeTypeSelect = row.items[0];
const codeTypeSelect = config.items[0];
expect(codeTypeSelect.name).toBe('codeType');
const setModel = vi.fn();
codeTypeSelect.onChange(undefined, 'data-source-method', { setModel });
@ -128,9 +132,8 @@ describe('CodeSelect', () => {
const wrapper = mount(CodeSelect, { props: baseProps() as any });
const container = wrapper.findComponent({ name: 'MContainer' });
const config = container.props('config') as any;
const row = config.items[0];
const codeIdCol = row.items[1];
const dsCol = row.items[2];
const codeIdCol = config.items[1];
const dsCol = config.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);
@ -141,9 +144,8 @@ describe('CodeSelect', () => {
const wrapper = mount(CodeSelect, { props: baseProps() as any });
const container = wrapper.findComponent({ name: 'MContainer' });
const config = container.props('config') as any;
const row = config.items[0];
const codeIdCol = row.items[1];
const dsCol = row.items[2];
const codeIdCol = config.items[1];
const dsCol = config.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');
@ -156,9 +158,8 @@ describe('CodeSelect', () => {
const wrapper = mount(CodeSelect, { props: baseProps() as any });
const container = wrapper.findComponent({ name: 'MContainer' });
const config = container.props('config') as any;
const row = config.items[0];
expect(row.items[1].notEditable()).toBe(true);
expect(row.items[2].notEditable()).toBe(true);
expect(config.items[1].notEditable()).toBe(true);
expect(config.items[2].notEditable()).toBe(true);
codeBlockService.getEditStatus.mockReturnValue(true);
dataSourceService.get.mockReturnValue(true);
});

View File

@ -526,9 +526,11 @@ describe('EventSelect', () => {
],
};
const wrapper = mount(EventSelect, { props: baseProps({ model: m }) as any });
const buttons = wrapper.findAll('button');
const deleteBtn = buttons[buttons.length - 1];
await deleteBtn.trigger('click');
// 用 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');
expect(m.events.length).toBe(1);
});
});

View File

@ -1,4 +1,5 @@
import {
ElAlert,
ElAutocomplete,
ElBadge,
ElButton,
@ -48,6 +49,7 @@ import {
} from 'element-plus';
import type {
AlertProps,
AutocompleteProps,
BadgeProps,
ButtonProps,
@ -104,6 +106,11 @@ const adapter: DesignPluginOptions = {
props: (props: AutocompleteProps) => props,
},
alert: {
component: ElAlert as any,
props: (props: AlertProps) => props,
},
badge: {
component: ElBadge as any,
props: (props: BadgeProps) => props,
@ -261,7 +268,7 @@ const adapter: DesignPluginOptions = {
select: {
component: ElSelect as any,
props: (props: SelectProps) => props,
props: (props: SelectProps) => ({ remoteShowSuffix: true, ...props }),
},
step: {

View File

@ -50,6 +50,9 @@ export interface FieldProps<T = any> {
size?: 'large' | 'default' | 'small';
lastValues?: Record<string, any>;
isCompare?: boolean;
text?: string;
labelWidth?: string | number;
labelPosition?: 'top' | 'left' | 'right';
}
/**
@ -110,6 +113,8 @@ export interface FormItem {
name?: string | number;
/** 额外的提示信息,和 help 类似,当提示文案同时出现时,可以使用这个。 */
extra?: string | FilterFunction<string>;
/** 额外的提示信息,和 extra 类似着重强调时用这个显示优先级高于extra*/
extraTips?: string;
/** 配置提示信息 */
tooltip?: ToolTipConfigType | FilterFunction<ToolTipConfigType>;
/** 是否置灰 */
@ -130,6 +135,8 @@ export interface FormItem {
trim?: boolean;
/** 默认值 */
defaultValue?: any | DefaultValueFunction;
/** 标题下方的额外提示信息 */
titleExtra?: string;
/** 表单验证规则 */
rules?: Rule[];
extensible?: boolean;
@ -139,6 +146,9 @@ export interface FormItem {
style?: Record<string, any>;
fieldStyle?: Record<string, any>;
labelPosition?: 'top' | 'left' | 'right';
flat?: boolean;
fixed?: boolean | 'left' | 'right';
operateColWidth?: number | string;
}
// #endregion FormItem
@ -386,6 +396,12 @@ export interface TextConfig extends FormItem, Input {
tooltip?: string;
/** 是否可清空 */
clearable?: boolean;
/**
*
* `<span>` `prepend` / `append`
* icon
*/
static?: boolean;
prepend?: string;
/** 后置元素,一般为标签或按钮 */
append?:
@ -393,7 +409,8 @@ export interface TextConfig extends FormItem, Input {
| {
text: string;
value?: 0 | 1;
type: 'button';
type: 'button' | 'icon';
extra?: string;
handler?: (
mForm: FormState | undefined,
data: {
@ -529,6 +546,7 @@ export interface SwitchConfig extends FormItem {
export interface RadioGroupConfig extends FormItem {
type: 'radio-group' | 'radioGroup';
childType?: 'default' | 'button';
iconSize?: string;
options: {
value: string | number | boolean;
text?: string;
@ -718,6 +736,7 @@ export interface TabPaneConfig<T = never> {
title: string;
lazy?: boolean;
labelWidth?: string;
titleExtra?: string;
items: FormConfig<T>;
display?: boolean | 'expand' | FilterFunction<boolean | 'expand'>;
onTabClick?: (mForm: FormState | undefined, tab: any, data: any) => void;
@ -772,6 +791,11 @@ export interface PanelConfig<T = never> extends FormItem, ContainerCommonConfig<
expand?: boolean;
title?: string;
schematic?: string;
/**
* panel / / / /
* panel header
*/
flat?: boolean;
}
// #endregion PanelConfig
@ -825,6 +849,8 @@ export interface TableConfig<T = never> extends TableGroupListCommonConfig {
/** 操作栏宽度 */
operateColWidth?: number | string;
pagination?: boolean;
/** 表格标题前缀 新增xxx,暂无xxx数据表明表格内容是什么 */
titlePrefix?: string;
/** 是否显示删除按钮 */
delete?: (model: any, index: number, values: any) => boolean | boolean;
copyable?: (model: any, data: any) => boolean | boolean;
@ -865,6 +891,13 @@ export interface GroupListConfig<T = never> extends TableGroupListCommonConfig {
title?: string | FilterFunction<string>;
itemExtra?: string | FilterFunction<string>;
expandAll?: boolean;
/**
* item //header 线
* + ////
* card `EventSelect` actions
* item 线
*/
flat?: boolean;
/**
*
*

View File

@ -70,6 +70,7 @@ export interface CodeSelectConfig extends FormItem {
// #region CodeSelectColConfig
export interface CodeSelectColConfig extends FormItem {
type: 'code-select-col';
/** 是否可以编辑代码块disable表示的是是否可以选择代码块 */
notEditable?: boolean | FilterFunction;
}

View File

@ -1,6 +1,10 @@
<template>
<TMagicForm
class="m-form"
:class="[
'm-form',
effectiveTheme ? `m-form--${effectiveTheme}` : '',
effectiveTheme ? `m-theme--${effectiveTheme}` : '',
]"
ref="tMagicForm"
:model="values"
:label-width="labelWidth"
@ -32,10 +36,22 @@
</template>
<script setup lang="ts">
import { computed, nextTick, provide, reactive, ref, shallowRef, toRaw, useTemplateRef, watch, watchEffect } from 'vue';
import {
computed,
inject,
nextTick,
provide,
reactive,
ref,
shallowRef,
toRaw,
useTemplateRef,
watch,
watchEffect,
} from 'vue';
import { cloneDeep, isEqual } from 'lodash-es';
import { TMagicForm, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
import { M_THEME_KEY, TMagicForm, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
import { setValueByKeyPath } from '@tmagic/utils';
import Container from './containers/Container.vue';
@ -116,6 +132,15 @@ const props = withDefaults(
* 通过 provide 下发对整棵表单的所有层级 Container 生效只需在 MForm 这一层传一次
*/
selfDiffFieldTypes?: string[] | ((_defaultTypes: string[]) => string[]);
/**
* 主题名称对应 `packages/form/src/theme/themes/<theme>/index.scss` 的目录名
*
* 设置后会在表单根元素上追加 `m-form--<theme>` 修饰类配合按需引入
* `@tmagic/form/dist/themes/<theme>.css` 即可启用主题样式
*
* 例如`<MForm theme="magic-admin" />` + `import '@tmagic/form/dist/themes/magic-admin.css'`
*/
theme?: string;
}>(),
{
config: () => [],
@ -149,6 +174,27 @@ const fields = new Map<string, any>();
const requestFuc = getConfig('request') as Function;
/**
* 当前表单生效的主题名称
* - 优先用本组件自己的 `props.theme`
* - 没设置时回退到最近祖先 `<MEditor>` / `<MForm>` provide 的主题便于内嵌于编辑器
* 时自动跟随外层主题无需在每个 `MForm` 上重复传 `theme`
*
* 同时把合并后的值再 provide 出去见下方 `provide(M_THEME_KEY, ...)` form 子树
* 里再嵌套的 portal 组件`TMagicPopover` 依然能拿到非空主题
*/
const ancestorTheme = inject(M_THEME_KEY, null);
const effectiveTheme = computed(() => props.theme || ancestorTheme?.value || '');
/**
* 拼到 `formState.popperClass` 上的主题修饰类 `m-theme--<theme>`
* 不带 `m-form` / `m-editor` 前缀因为 Element Plus 弹层节点本身既不是 form 也不是 editor
*
* 这条类会随所有读 `mForm.popperClass` 的字段Select / DateTime / Cascader 下发到
* Element Plus `popper-class` portal 节点也命中 `m-theme--<theme>` 上的 CSS 变量
*/
const themeClass = computed(() => (effectiveTheme.value ? `m-theme--${effectiveTheme.value}` : ''));
/**
* formState 实现说明
*
@ -165,13 +211,21 @@ const requestFuc = getConfig('request') as Function;
* 把最新值刷进 formState
* - accessor 描述符`{ get stage() { return ... } }`按原样写入调用方可以控制
* 读时求值每次读取都会重新执行 getter
*
* 4. `popperClass` 会自动拼接 `themeClass`调用方传入的 `popperClass` + 当前主题
* 修饰类含祖先 `<MEditor>` provide 的主题这样所有透传到 Element Plus 弹层
* `popper-class` 的字段Select / DateTime / Cascader 能自带主题作用域
*/
const formState: FormState = reactive<FormState>({
get keyProp() {
return props.keyProp;
},
get popperClass() {
return props.popperClass;
const userClass = props.popperClass ?? '';
const tc = themeClass.value;
if (!userClass) return tc;
if (!tc) return userClass;
return `${userClass} ${tc}`;
},
get config() {
return props.config;
@ -250,6 +304,13 @@ watchEffect(async (onCleanup) => {
provide('mForm', formState);
/**
* 把生效主题自身或祖先 provide 出去 form 子树内含 `Teleport` 的组件
* `TMagicPopover`在传送目标上挂 `m-theme--<theme>`
* 详见 `@tmagic/design/theme.ts`
*/
provide(M_THEME_KEY, effectiveTheme);
provide('formInline', props.inline);
// provide Container inject prop
// getter props
provide(FORM_DIFF_CONFIG_KEY, {

View File

@ -1,7 +1,7 @@
<template>
<TMagicDialog
v-model="dialogVisible"
class="m-form-dialog"
:class="['m-form-dialog', effectiveTheme ? `m-theme--${effectiveTheme}` : '']"
top="20px"
append-to-body
:title="title"
@ -34,6 +34,7 @@
:use-field-text-in-error="useFieldTextInError"
:type-match-valid="typeMatchValid"
:extend-state="extendState"
:theme="effectiveTheme"
@change="changeHandler"
></Form>
<slot></slot>
@ -66,9 +67,9 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { computed, inject, provide, ref } from 'vue';
import { TMagicButton, TMagicCol, TMagicDialog, TMagicRow } from '@tmagic/design';
import { M_THEME_KEY, TMagicButton, TMagicCol, TMagicDialog, TMagicRow } from '@tmagic/design';
import Form from './Form.vue';
import { ContainerChangeEventData, FormConfig, FormState, FormValue, StepConfig } from './schema';
@ -104,6 +105,12 @@ const props = withDefaults(
useFieldTextInError?: boolean;
/** 透传给内部 `MForm`,用于扩展 `formState`(如注入 `$message` / `$store` 等) */
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/**
* 主题名优先级传入 `theme` prop > 祖先 `provide(M_THEME_KEY)` > 空串
* 计算结果会再次 `provide` 出去使得 Dialog Teleport body 内部子树
* 包括 `MForm` 以及更深的 popover / dropdown仍能拿到主题
*/
theme?: string;
}>(),
{
config: () => [],
@ -118,6 +125,10 @@ const props = withDefaults(
},
);
const ancestorTheme = inject(M_THEME_KEY, null);
const effectiveTheme = computed(() => props.theme || ancestorTheme?.value || '');
provide(M_THEME_KEY, effectiveTheme);
const emit = defineEmits(['close', 'submit', 'error', 'change']);
const form = ref<InstanceType<typeof Form>>();

View File

@ -1,8 +1,8 @@
<template>
<TMagicDrawer
class="m-form-drawer"
:class="['m-form-drawer', effectiveTheme ? `m-theme--${effectiveTheme}` : '']"
ref="drawer"
v-model="visible"
v-model="dialogVisible"
:title="title"
:close-on-press-escape="closeOnPressEscape"
:append-to-body="true"
@ -16,7 +16,7 @@
@close="closeHandler"
@closed="closedHandler"
>
<div v-if="visible" ref="drawerBody" class="m-drawer-body">
<div v-if="dialogVisible" ref="drawerBody" class="m-drawer-body">
<Form
ref="form"
:size="size"
@ -31,6 +31,7 @@
:use-field-text-in-error="useFieldTextInError"
:type-match-valid="typeMatchValid"
:extend-state="extendState"
:theme="effectiveTheme"
@change="changeHandler"
></Form>
<slot></slot>
@ -57,18 +58,18 @@
</template>
<script setup lang="ts">
import { ref, watchEffect } from 'vue';
import { computed, inject, provide, ref, watchEffect } from 'vue';
import { TMagicButton, TMagicCol, TMagicDrawer, TMagicRow } from '@tmagic/design';
import { M_THEME_KEY, TMagicButton, TMagicCol, TMagicDrawer, TMagicRow } from '@tmagic/design';
import Form from './Form.vue';
import type { ContainerChangeEventData, FormConfig, FormState, FormValue } from './schema';
defineOptions({
name: 'MFormDialog',
name: 'MFormDrawer',
});
withDefaults(
const props = withDefaults(
defineProps<{
config?: FormConfig;
values?: Object;
@ -92,6 +93,12 @@ withDefaults(
beforeClose?: (_done: (_cancel?: boolean) => void) => void;
/** 透传给内部 `MForm`,用于扩展 `formState`(如注入 `$message` / `$store` 等) */
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/**
* 主题名优先级传入 `theme` prop > 祖先 `provide(M_THEME_KEY)` > 空串
* 计算结果会再次 `provide` 出去使得 Drawer Teleport body 内部子树
* 包括 `MForm` 以及更深的 popover / dropdown仍能拿到主题
*/
theme?: string;
}>(),
{
closeOnPressEscape: true,
@ -102,12 +109,16 @@ withDefaults(
},
);
const ancestorTheme = inject(M_THEME_KEY, null);
const effectiveTheme = computed(() => props.theme || ancestorTheme?.value || '');
provide(M_THEME_KEY, effectiveTheme);
const emit = defineEmits(['close', 'closed', 'submit', 'error', 'change', 'open', 'opened']);
const drawer = ref<InstanceType<typeof TMagicDrawer>>();
const form = ref<InstanceType<typeof Form>>();
const drawerBody = ref<HTMLDivElement>();
const visible = ref(false);
const dialogVisible = ref(false);
const saveFetch = ref(false);
const bodyHeight = ref(0);
@ -148,25 +159,42 @@ const closedHandler = () => {
};
const show = () => {
visible.value = true;
dialogVisible.value = true;
};
const hide = () => {
visible.value = false;
dialogVisible.value = false;
};
const close = () => {
dialogVisible.value = false;
};
const cancel = () => {
hide();
};
/** 用于关闭 Drawer, 该方法会调用传入的 before-close 方法 */
const handleClose = () => {
drawer.value?.handleClose();
};
const save = async () => {
try {
const changeRecords = [...(form.value?.changeRecords || [])];
const values = await form.value?.submitForm();
emit('submit', values, { changeRecords });
} catch (e) {
emit('error', e);
}
};
defineExpose({
form,
saveFetch,
bodyHeight,
close,
show,
hide,
save,
cancel,
handleClose,
});
</script>

View File

@ -35,6 +35,9 @@
:label-title="config.labelTitle"
:text="text"
></FormLabel>
<span class="m-form-tip m-form-title-extra" v-if="config.titleExtra && config.labelPosition === 'top'">
{{ config.titleExtra }}</span
>
</slot>
</template>
@ -222,17 +225,23 @@
</template>
</template>
<div style="text-align: center" v-if="config.expand && type !== 'fieldset'">
<TMagicButton type="primary" size="small" :disabled="false" link @click="expandHandler">{{
expand ? '收起配置' : '展开更多配置'
}}</TMagicButton>
<div class="m-form-container-expand" v-if="config.expand && type !== 'fieldset'">
<TMagicButton
type="primary"
size="small"
:disabled="false"
link
@click="expandHandler"
:icon="expand ? ArrowUp : ArrowDown"
>{{ expand ? '收起配置' : '展开更多配置' }}</TMagicButton
>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, inject, readonly, ref, toRaw, watch, watchEffect } from 'vue';
import { WarningFilled } from '@element-plus/icons-vue';
import { ArrowDown, ArrowUp, WarningFilled } from '@element-plus/icons-vue';
import { isEqual } from 'lodash-es';
import { TMagicButton, TMagicFormItem, TMagicIcon, TMagicTooltip } from '@tmagic/design';
@ -440,8 +449,10 @@ const formItemProps = computed(() => ({
prop: itemProp.value,
labelWidth: itemLabelWidth.value,
labelPosition: props.config.labelPosition,
rules: rule.value,
extra: filterFunction(mForm, props.config.extra, props),
extraTips: props.config.extraTips,
}));
const itemLabelWidth = computed(() => props.config.labelWidth ?? props.labelWidth);

View File

@ -2,7 +2,7 @@
<div class="m-fields-group-list">
<div v-if="config.extra" v-html="config.extra" style="color: rgba(0, 0, 0, 0.45)"></div>
<div v-if="!model[name] || !model[name].length" class="el-table__empty-block">
<span class="el-table__empty-text t-table__empty">暂无数据</span>
<span class="el-table__empty-text t-table__empty">暂无{{ config.titlePrefix || '' }}数据</span>
</div>
<MFieldsGroupListItem

View File

@ -1,26 +1,29 @@
<template>
<TMagicCard class="m-fields-group-list-item" :body-style="{ display: expand ? 'block' : 'none' }">
<TMagicCard class="m-fields-group-list-item" :flat="config.flat" :body-style="{ display: expand ? 'block' : 'none' }">
<template #header>
<div>
<TMagicButton link :disabled="disabled" @click="expandHandler">
<TMagicIcon><CaretBottom v-if="expand" /><CaretRight v-else /></TMagicIcon><span v-html="title"></span>
<div class="m-fields-group-list-item-header">
<TMagicButton link :disabled="disabled" @click="expandHandler" class="expand-button">
<TMagicIcon><ArrowDown v-if="expand" /><ArrowRight v-else /></TMagicIcon>
</TMagicButton>
<TMagicButton
v-if="!isCompare"
v-show="showDelete"
type="danger"
size="small"
link
:icon="Delete"
:disabled="disabled"
@click="removeHandler"
></TMagicButton>
<span v-html="title"></span>
<TMagicTooltip :content="`删除 ${title}`">
<TMagicButton
v-if="!isCompare"
v-show="showDelete"
size="default"
link
class="delete-button"
:icon="Delete"
:disabled="disabled"
@click="removeHandler"
></TMagicButton>
</TMagicTooltip>
<TMagicButton
v-if="copyable && !isCompare"
link
size="small"
size="default"
type="primary"
:icon="DocumentCopy"
:disabled="disabled"
@ -32,18 +35,18 @@
<TMagicButton
v-show="index !== 0"
link
size="small"
size="default"
:disabled="disabled"
:icon="CaretTop"
:icon="Top"
@click="changeOrder(-1)"
>上移</TMagicButton
>
<TMagicButton
v-show="index !== length - 1"
link
size="small"
size="default"
:disabled="disabled"
:icon="CaretBottom"
:icon="Bottom"
@click="changeOrder(1)"
>下移</TMagicButton
>
@ -107,9 +110,9 @@
<script setup lang="ts">
import { computed, inject, ref } from 'vue';
import { CaretBottom, CaretRight, CaretTop, Delete, DocumentCopy, Position } from '@element-plus/icons-vue';
import { ArrowDown, ArrowRight, Bottom, Delete, DocumentCopy, Position, Top } from '@element-plus/icons-vue';
import { TMagicButton, TMagicCard, TMagicIcon, TMagicInputNumber, TMagicPopover } from '@tmagic/design';
import { TMagicButton, TMagicCard, TMagicIcon, TMagicInputNumber, TMagicPopover, TMagicTooltip } from '@tmagic/design';
import type { ContainerChangeEventData, FormState, GroupListConfig } from '../schema';
import { filterFunction } from '../utils/form';
@ -152,7 +155,10 @@ const rowConfig = computed(() => ({
const title = computed(() => {
if (props.config.titleKey && props.model[props.config.titleKey]) {
return props.model[props.config.titleKey];
const { titlePrefix } = props.config;
return titlePrefix
? `${titlePrefix} ${String(props.index + 1)}: ${props.model[props.config.titleKey]}`
: `${props.model[props.config.titleKey]}`;
}
if (props.config.title) {

View File

@ -3,10 +3,17 @@
v-if="items && items.length"
class="box-card m-form-panel"
:body-style="{ display: expand ? 'block' : 'none' }"
:flat="config.flat"
>
<template #header>
<div style="width: 100%; display: flex; align-items: center">
<TMagicButton style="padding: 0" link :icon="expand ? CaretBottom : CaretRight" @click="expand = !expand">
<TMagicButton
style="padding: 0; margin-right: 10px"
link
:icon="expand ? ArrowDown : ArrowRight"
@click="expand = !expand"
v-if="!hideExpand"
>
</TMagicButton>
<slot name="header">
<span style="cursor: pointer" @click="expand = !expand">{{ filter(config.title) }}</span>
@ -62,7 +69,7 @@
<script setup lang="ts">
import { computed, inject, ref } from 'vue';
import { CaretBottom, CaretRight } from '@element-plus/icons-vue';
import { ArrowDown, ArrowRight } from '@element-plus/icons-vue';
import { TMagicButton, TMagicCard } from '@tmagic/design';
@ -85,6 +92,7 @@ const props = defineProps<{
prop?: string;
size?: string;
disabled?: boolean;
hideExpand?: boolean;
}>();
const emit = defineEmits<{
@ -107,7 +115,6 @@ const onAddDiffCount = () => emit('addDiffCount');
defineExpose({
getExpand: () => expand.value,
setExpand: (v: boolean) => {
expand.value = v;
},

View File

@ -37,11 +37,17 @@
:size="addButtonSize"
:plain="displayMode === 'table'"
:icon="Plus"
text
:disabled="disabled"
v-bind="currentConfig.addButtonConfig?.props || { type: 'primary' }"
@click="newHandler"
>
{{ currentConfig.addButtonConfig?.text || (displayMode === 'table' ? '新增一行' : '新增') }}
{{
currentConfig.addButtonConfig?.text ||
(displayMode === 'table'
? `新增一行${groupListConfig.titlePrefix || ''}`
: `新增${groupListConfig.titlePrefix || ''}`)
}}
</TMagicButton>
</template>
</component>

View File

@ -9,15 +9,18 @@
>
</TMagicButton>
</TMagicTooltip>
<TMagicButton
v-show="showDelete(index + 1 + currentPage * pageSize - 1)"
size="small"
type="danger"
link
title="删除"
:icon="config.deleteActionButtonIcon || Delete"
:icon="flat ? undefined : config.deleteActionButtonIcon || Delete"
@click="removeHandler(index + 1 + currentPage * pageSize - 1)"
></TMagicButton>
>
<template v-if="flat">删除</template>
</TMagicButton>
<TMagicButton
v-if="copyable(index + 1 + currentPage * pageSize - 1)"
@ -25,10 +28,12 @@
size="small"
type="primary"
title="复制"
:icon="config.copyActionButtonIcon || DocumentCopy"
:icon="flat ? undefined : config.copyActionButtonIcon || DocumentCopy"
:disabled="disabled"
@click="copyHandler(index + 1 + currentPage * pageSize - 1)"
></TMagicButton>
>
<template v-if="flat">复制</template>
</TMagicButton>
</template>
<script setup lang="ts">
@ -53,6 +58,7 @@ const props = defineProps<{
row: any;
prop?: string;
sortKey?: string;
flat?: boolean;
}>();
const mForm = inject<FormState | undefined>('mForm');

View File

@ -27,6 +27,7 @@
:key="updateKey"
@select="selectHandle"
@sort-change="sortChangeHandler"
:empty-text="`暂无${config.titlePrefix || ''}数据`"
></TMagicTable>
</TMagicTooltip>

View File

@ -6,6 +6,7 @@ import { type TableColumnOptions, TMagicIcon, TMagicTooltip } from '@tmagic/desi
import type { FormItemConfig, FormState, TableColumnConfig } from '@tmagic/form-schema';
import type { ContainerChangeEventData } from '../../schema';
import { isGlobalFlat } from '../../utils/config';
import { display as displayFunc, getDataByPage, sortArray } from '../../utils/form';
import Container from '../Container.vue';
@ -111,23 +112,26 @@ export const useTableColumns = (
},
});
}
let actionFixed: 'left' | 'right' | undefined = props.config.fixed === false ? undefined : 'left';
const defaultFixed: 'left' | 'right' = isGlobalFlat.value ? 'right' : 'left';
let actionFixed: 'left' | 'right' | undefined = props.config.fixed === false ? undefined : defaultFixed;
if (typeof props.config.fixed === 'string' && ['left', 'right'].includes(props.config.fixed)) {
actionFixed = props.config.fixed;
}
const actionFlat = props.config.flat === undefined && isGlobalFlat.value ? true : props.config.flat;
const actionColumn = {
props: {
label: '操作',
fixed: actionFixed,
width: props.config.operateColWidth ?? (props.config.dropSortHandle && props.config.dropSort ? 132 : 112),
align: 'center',
},
cell: ({ row, $index }: any) =>
h(ActionsColumn, {
row,
flat: actionFlat,
index: $index,
model: props.model,
config: props.config,

View File

@ -1,5 +1,16 @@
<template>
<a v-if="config.href && !disabled" target="_blank" :href="href" :style="config.css || {}">{{ displayText }}</a>
<a
target="_blank"
:href="href"
v-if="isGlobalFlat && config.href && !disabled"
class="magic-admin-link"
:style="config.css || {}"
>
<TMagicButton link type="primary">
<TMagicIcon><Notebook /></TMagicIcon>{{ displayText }}
</TMagicButton>
</a>
<a v-else-if="config.href && !disabled" target="_blank" :href="href" :style="config.css || {}">{{ displayText }}</a>
<span v-else-if="config.href && disabled" :style="config.disabledCss || {}">{{ displayText }}</span>
<div v-else class="m-fields-link">
<TMagicButton link type="primary" @click="editHandler">点击编辑</TMagicButton>
@ -18,18 +29,20 @@
<script lang="ts" setup>
import { computed, inject, readonly, ref } from 'vue';
import { Notebook } from '@element-plus/icons-vue';
import { TMagicButton } from '@tmagic/design';
import { TMagicButton, TMagicIcon } from '@tmagic/design';
import FormDialog from '../FormDialog.vue';
import type { FieldProps, FormState, LinkConfig } from '../schema';
import { isGlobalFlat } from '../utils/config';
import { useAddField } from '../utils/useAddField';
defineOptions({
name: 'MFormLink',
});
const props = defineProps<FieldProps<LinkConfig>>();
const props = defineProps<FieldProps<LinkConfig> & { theme?: string }>();
const emit = defineEmits(['change']);

View File

@ -3,7 +3,7 @@
v-if="model"
v-model="value"
clearable
controls-position="right"
:controls-position="isGlobalFlat ? '' : 'right'"
:size="size"
:max="config.max"
:min="config.min"
@ -21,6 +21,7 @@ import { inject, ref, watch } from 'vue';
import { TMagicInputNumber } from '@tmagic/design';
import type { FieldProps, FormState, NumberConfig } from '../schema';
import { isGlobalFlat } from '../utils/config';
import { useAddField } from '../utils/useAddField';
defineOptions({

View File

@ -1,5 +1,11 @@
<template>
<TMagicRadioGroup v-if="model" :model-value="model[name]" :size="size" :disabled="disabled">
<TMagicRadioGroup
v-if="model"
:model-value="model[name]"
:size="size"
:disabled="disabled"
class="icon-{{ iconSize }}"
>
<component
v-for="option in config.options"
:is="itemComponent"
@ -8,9 +14,14 @@
@click="clickHandler(option.value)"
>
<TMagicTooltip :disabled="!Boolean(option.tooltip)" placement="top-start" :content="option.tooltip">
<div>
<TMagicIcon v-if="option.icon" :size="iconSize"><component :is="option.icon"></component></TMagicIcon>
<span v-if="option.text">{{ option.text }}</span>
<div class="m-fields-radio-group__option">
<TMagicIcon
v-if="option.icon"
:size="iconSize"
:class="{ 'm-fields-radio-group__icon_with_text': !!option.text }"
><component :is="option.icon"></component
></TMagicIcon>
<span v-if="option.text" class="m-fields-radio-group__text">{{ option.text }}</span>
</div>
</TMagicTooltip>
</component>
@ -45,13 +56,17 @@ const clickHandler = (item: string | number | boolean) => {
useAddField(props.prop);
// 稿
const iconSize = computed(() => {
if (props.config.iconSize) {
return props.config.iconSize;
}
if (props.size === 'small') {
return '12';
return '14';
}
if (props.size === 'large') {
return '16';
}
return '14';
return '16';
});
</script>

View File

@ -1,6 +1,31 @@
<template>
<div class="m-fields-text">
<div :class="['m-fields-text', { 'm-fields-text--static': config.static }]">
<div v-if="config.disabled"></div>
<!-- 新增静态展示文本样式 -->
<template v-if="config.static">
<span v-if="config.prepend" class="m-fields-text__prepend">{{ config.prepend }}</span>
<span class="m-fields-text__value">{{ value }}</span>
<template v-if="appendConfig">
<TMagicButton
v-if="appendConfig.type === 'button'"
style="color: #0056ea"
:size="size"
@click.prevent="buttonClickHandler"
>
{{ appendConfig.text }}
</TMagicButton>
<img
v-else-if="appendConfig.type === 'icon'"
class="m-fields-text__icon"
:src="appendConfig.text"
@click="buttonClickHandler"
/>
<span v-else>{{ appendConfig.text }}</span>
</template>
</template>
<TMagicInput
v-else
v-model="value"
ref="input"
:clearable="config.clearable ?? true"
@ -17,17 +42,18 @@
<template #append v-if="appendConfig">
<TMagicButton
v-if="appendConfig.type === 'button'"
style="color: #409eff"
style="color: #0056ea"
:size="size"
@click.prevent="buttonClickHandler"
>
{{ appendConfig.text }}
</TMagicButton>
<img v-else-if="appendConfig.type === 'icon'" :src="appendConfig.text" @click="buttonClickHandler" />
<span v-else>{{ appendConfig.text }}</span>
</template>
</TMagicInput>
<Teleport to="body">
<Teleport to="body" v-if="!config.static">
<div v-if="popoverVisible" class="tmagic-form-text-popper m-form-item__content" ref="popoverEl">
<div class="m-form-validate__warning">输入内容前后有空格是否移除空格</div>
<div style="display: flex; justify-content: flex-end">
@ -80,28 +106,26 @@ watch(
);
const appendConfig = computed(() => {
if (typeof props.config.append === 'string') {
const { append } = props.config;
// append
if (typeof append === 'string') {
return {
type: 'text',
text: props.config.append,
text: append,
handler: undefined,
};
}
if (typeof props.config.append === 'object') {
if (typeof props.config.append?.handler === 'function') {
return {
type: 'button',
text: props.config.append.text,
handler: props.config.append.handler,
};
}
if (props.config.append) {
if (props.config.append.value === 0) {
return false;
}
return props.config.append;
}
// schema `type` `'button' | 'icon'``handler`
//
// handler `type: 'button'` schema
// `type: 'icon'` `@editor/utils/props.ts` ID
// `type` button / icon / text
if (typeof append === 'object' && append) {
// `value === 0` 沿 append
if (append.value === 0) return false;
return append;
}
return false;

View File

@ -50,11 +50,13 @@ import { setConfig } from './utils/config';
import { registerTypeMatchRules, type TypeMatchValidator } from './utils/typeMatch';
import Form from './Form.vue';
import FormDialog from './FormDialog.vue';
import FormDrawer from './FormDrawer.vue';
import './theme/index.scss';
// #region FormInstallOptions
export interface FormInstallOptions {
flat?: boolean;
/** 自定义字段 type 的 typeMatch 校验规则,可覆盖内置规则或扩展业务字段 */
typeMatchRules?: Record<string, TypeMatchValidator>;
[key: string]: any;
@ -76,6 +78,7 @@ export default {
app.component('m-form', Form);
app.component('m-form-dialog', FormDialog);
app.component('m-form-drawer', FormDrawer);
app.component('m-form-container', Container);
app.component('m-form-fieldset', Fieldset);
app.component('m-form-group-list', TableGroupList);

View File

@ -22,4 +22,7 @@
background: #f7dadd;
}
}
.m-form-title-extra {
margin-left: 8px;
}
}

View File

@ -1,3 +1,54 @@
.m-container-fieldset {
&.no-border-fieldset {
&.fieldset-in-row {
> fieldset.m-fieldset {
legend {
left: 0;
}
}
}
> fieldset.m-fieldset {
border: 0;
legend {
position: relative;
left: -5px;
font-size: 16px;
font-weight: 500;
line-height: 24px;
padding: 0;
}
}
}
&.fieldset-in-card-fit {
border-bottom: 1px solid #e6e6e6;
> fieldset.m-fieldset {
padding: 16px 0 16px 0 !important;
border: 0;
margin-top: 0;
margin-bottom: 0;
legend {
position: relative;
top: 10px;
background-color: transparent;
}
}
&.fieldset-in-card-fit-last {
padding-bottom: 0 !important;
border-bottom: 0;
}
&.fieldset-in-card-fit-first {
padding-top: 0 !important;
}
&:last-child {
padding-bottom: 0 !important;
border-bottom: 0;
}
&:first {
padding-top: 0 !important;
}
}
}
fieldset.m-fieldset {
position: relative;
border: 1px solid rgb(229, 229, 229);

View File

@ -53,3 +53,9 @@
margin-bottom: var(--td-comp-margin-xxl);
}
}
.m-form-container {
.m-form-container-expand {
text-align: center;
}
}

View File

@ -1,4 +1,8 @@
.m-fields-group-list {
.m-fields-group-list-item.tmagic-design-card--flat:last-child {
border: 0;
}
.el-button--text {
padding: 0;
margin-bottom: 7px;
@ -19,14 +23,11 @@
&:last-of-type {
border-bottom: 0;
}
}
.tmagic-design-card {
.el-card__header {
padding: 5px 20px;
}
.t-card__header {
padding: 5px 0;
.m-fields-group-list-item-header {
display: flex;
align-items: center;
gap: 8px;
}
}
@ -34,5 +35,6 @@
display: flex;
justify-content: space-between;
margin-top: 10px;
margin-bottom: 16px;
}
}

View File

@ -13,3 +13,4 @@
@use "./tabs.scss";
@use "./number-range.scss";
@use "./form-box.scss";
@use "./radio-group.scss";

View File

@ -1,3 +1,20 @@
div.m-fields-link {
width: fit-content;
}
a.magic-admin-link {
width: calc(100%);
box-sizing: border-box;
border-radius: 4px;
border: 1px solid #e6e6e6;
background-color: #fafafa;
padding: 0 8px;
height: 28px;
display: inline-flex;
gap: 4px;
align-items: center;
text-decoration: none;
i {
margin-right: 4px;
}
}

View File

@ -4,7 +4,7 @@
background: #f2f6fc;
}
a {
color: #409eff;
color: var(--el-color-primary, #409eff);
}
}

Some files were not shown because too many files have changed in this diff Show More