mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-07-23 10:05:42 +08:00
447 lines
12 KiB
TypeScript
447 lines
12 KiB
TypeScript
/* eslint-disable @typescript-eslint/naming-convention */
|
||
/*
|
||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||
*
|
||
* Copyright (C) 2025 Tencent. All rights reserved.
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
|
||
import type { AppContext } from 'vue';
|
||
|
||
import {
|
||
HookType,
|
||
NODE_CONDS_KEY,
|
||
NODE_CONDS_RESULT_KEY,
|
||
NODE_DISABLE_CODE_BLOCK_KEY,
|
||
NODE_DISABLE_DATA_SOURCE_KEY,
|
||
} from '@tmagic/core';
|
||
import { tMagicMessage } from '@tmagic/design';
|
||
import type {
|
||
ChildConfig,
|
||
DisplayCondsConfig,
|
||
FormConfig,
|
||
FormState,
|
||
FormValue,
|
||
TabConfig,
|
||
TabPaneConfig,
|
||
} from '@tmagic/form';
|
||
import { validateForm } from '@tmagic/form';
|
||
|
||
import type { Services } from '@editor/type';
|
||
|
||
export const arrayOptions = [
|
||
{ text: '包含', value: 'include' },
|
||
{ text: '不包含', value: 'not_include' },
|
||
];
|
||
|
||
export const eqOptions = [
|
||
{ text: '等于', value: '=' },
|
||
{ text: '不等于', value: '!=' },
|
||
];
|
||
|
||
export const numberOptions = [
|
||
{ text: '大于', value: '>' },
|
||
{ text: '大于等于', value: '>=' },
|
||
{ text: '小于', value: '<' },
|
||
{ text: '小于等于', value: '<=' },
|
||
{ text: '在范围内', value: 'between' },
|
||
{ text: '不在范围内', value: 'not_between' },
|
||
];
|
||
|
||
export const booleanOptions = [
|
||
{ text: '是', value: 'is' },
|
||
{ text: '不是', value: 'not' },
|
||
];
|
||
|
||
/** 按字段类型返回条件运算符选项(UI 与 typeMatch 校验共用) */
|
||
export const getCondOpOptionsByFieldType = (type: string) => {
|
||
if (type === 'array') {
|
||
return arrayOptions;
|
||
}
|
||
|
||
if (type === 'boolean' || type === 'null') {
|
||
return booleanOptions;
|
||
}
|
||
|
||
if (type === 'number') {
|
||
return [...eqOptions, ...numberOptions];
|
||
}
|
||
|
||
if (type === 'string') {
|
||
return [...arrayOptions, ...eqOptions];
|
||
}
|
||
|
||
return [...arrayOptions, ...eqOptions, ...numberOptions];
|
||
};
|
||
|
||
export const styleTabConfig: TabPaneConfig = {
|
||
title: '样式',
|
||
lazy: true,
|
||
display: ({ services }: any) => !(services?.uiService?.get('showStylePanel') ?? true),
|
||
items: [
|
||
{
|
||
name: 'style',
|
||
labelWidth: '100px',
|
||
type: 'style-setter',
|
||
items: [
|
||
{
|
||
names: [
|
||
'display',
|
||
'flexDirection',
|
||
'justifyContent',
|
||
'alignItems',
|
||
'flexWrap',
|
||
'marginTop',
|
||
'marginRight',
|
||
'marginBottom',
|
||
'marginLeft',
|
||
'paddingTop',
|
||
'paddingRight',
|
||
'paddingBottom',
|
||
'paddingLeft',
|
||
'width',
|
||
'height',
|
||
'overflow',
|
||
'fontSize',
|
||
'lineHeight',
|
||
'fontWeight',
|
||
'color',
|
||
'textAlign',
|
||
'backgroundColor',
|
||
'backgroundImage',
|
||
'backgroundSize',
|
||
'backgroundPosition',
|
||
'backgroundRepeat',
|
||
'position',
|
||
'zIndex',
|
||
'top',
|
||
'right',
|
||
'bottom',
|
||
'left',
|
||
'borderRadius',
|
||
'borderTopWidth',
|
||
'borderTopStyle',
|
||
'borderTopColor',
|
||
'borderRightColor',
|
||
'borderRightWidth',
|
||
'borderRightStyle',
|
||
'borderRightColor',
|
||
'borderBottomWidth',
|
||
'borderBottomStyle',
|
||
'borderBottomColor',
|
||
'borderLeftStyle',
|
||
'borderLeftWidth',
|
||
'borderLeftColor',
|
||
'borderWidth',
|
||
'borderStyle',
|
||
'borderColor',
|
||
'opacity',
|
||
],
|
||
} as unknown as ChildConfig,
|
||
{
|
||
name: 'transform',
|
||
defaultValue: () => ({}),
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|
||
|
||
export const eventTabConfig: TabPaneConfig = {
|
||
title: '事件',
|
||
lazy: true,
|
||
items: [
|
||
{
|
||
name: 'events',
|
||
src: 'component',
|
||
labelWidth: '100px',
|
||
type: 'event-select',
|
||
rules: [{ typeMatch: true }],
|
||
},
|
||
],
|
||
};
|
||
|
||
export const advancedTabConfig: TabPaneConfig = {
|
||
title: '高级',
|
||
lazy: true,
|
||
items: [
|
||
{
|
||
name: NODE_DISABLE_CODE_BLOCK_KEY,
|
||
text: '禁用代码块',
|
||
type: 'switch',
|
||
defaultValue: false,
|
||
extra: '开启后,配置的代码块将不会被执行',
|
||
},
|
||
{
|
||
name: NODE_DISABLE_DATA_SOURCE_KEY,
|
||
text: '禁用数据源',
|
||
type: 'switch',
|
||
defaultValue: false,
|
||
extra: '开启后,组件内配置的数据源相关配置将不会被编译,显隐条件将失效',
|
||
},
|
||
{
|
||
name: 'created',
|
||
text: 'created',
|
||
labelPosition: 'top',
|
||
type: 'code-select',
|
||
extra: '组件初始化时执行',
|
||
rules: [
|
||
{ typeMatch: true, trigger: 'change' },
|
||
{
|
||
validator: ({ value, callback }: any) => {
|
||
if (value && value.hookType !== HookType.CODE) {
|
||
return callback('hookType 必须是 code');
|
||
}
|
||
callback();
|
||
},
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'mounted',
|
||
text: 'mounted',
|
||
labelPosition: 'top',
|
||
type: 'code-select',
|
||
extra: '组件挂载到dom时执行',
|
||
rules: [
|
||
{ typeMatch: true, trigger: 'change' },
|
||
{
|
||
validator: ({ value, callback }: any) => {
|
||
if (value && value.hookType !== HookType.CODE) {
|
||
return callback('hookType 必须是 code');
|
||
}
|
||
callback();
|
||
},
|
||
},
|
||
],
|
||
},
|
||
{
|
||
name: 'display',
|
||
text: 'display',
|
||
extra: '控制组件是否渲染,关系的代码块返回值为false时不渲染',
|
||
labelPosition: 'top',
|
||
type: 'code-select',
|
||
rules: [
|
||
{ typeMatch: true, trigger: 'change' },
|
||
{
|
||
validator: ({ value, callback }: any) => {
|
||
if (value && value.hookType !== HookType.CODE) {
|
||
return callback('hookType 必须是 code');
|
||
}
|
||
callback();
|
||
},
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|
||
|
||
export const displayTabConfig: TabPaneConfig<DisplayCondsConfig> = {
|
||
title: '显示条件',
|
||
display: (_state, { model }) => model.type !== 'page',
|
||
items: [
|
||
{
|
||
name: NODE_CONDS_RESULT_KEY,
|
||
type: 'select',
|
||
text: '条件成立时',
|
||
defaultValue: false,
|
||
options: [
|
||
{ text: '显示', value: false },
|
||
{ text: '隐藏', value: true },
|
||
],
|
||
extra: (_state, { model }) =>
|
||
`条件成立时${model[NODE_CONDS_RESULT_KEY] ? '隐藏' : '显示'},不成立时${model[NODE_CONDS_RESULT_KEY] ? '显示' : '隐藏'};<br />同一条件组内的所有条件配置同时成立时表示该条件组成立,任意一个条件组成立时表示条件成立(条件组内为且的关系,条件组间为或的关系);<br />条件为空时表示成立;`,
|
||
},
|
||
{
|
||
type: 'display-conds',
|
||
name: NODE_CONDS_KEY,
|
||
titlePrefix: '条件组',
|
||
defaultValue: [],
|
||
rules: [{ typeMatch: true }],
|
||
},
|
||
],
|
||
};
|
||
|
||
/**
|
||
* 统一为组件属性表单加上事件、高级、样式配置
|
||
* @param config 组件属性配置
|
||
* @returns Object
|
||
*/
|
||
export const fillConfig = (
|
||
config: FormConfig = [],
|
||
{
|
||
labelWidth = '80px',
|
||
disabledDataSource = false,
|
||
disabledCodeBlock = false,
|
||
}: { labelWidth?: string; disabledDataSource?: boolean; disabledCodeBlock?: boolean } = {},
|
||
): FormConfig => {
|
||
const propsConfig: FormConfig = [];
|
||
|
||
// 组件类型,必须要有
|
||
if (!config.find((item) => 'name' in item && item.name === 'type')) {
|
||
propsConfig.push({
|
||
text: 'type',
|
||
name: 'type',
|
||
type: 'hidden',
|
||
});
|
||
}
|
||
|
||
if (!config.find((item) => 'name' in item && item.name === 'id')) {
|
||
// 组件id,必须要有
|
||
propsConfig.push({
|
||
name: 'id',
|
||
text: 'ID',
|
||
type: 'text',
|
||
disabled: true,
|
||
append: {
|
||
type: 'button',
|
||
text: '复制',
|
||
handler: (vm, { model }) => {
|
||
navigator.clipboard
|
||
.writeText(`${model.id}`)
|
||
.then(() => {
|
||
tMagicMessage.success('已复制');
|
||
})
|
||
.catch(() => {
|
||
tMagicMessage.error('复制失败');
|
||
});
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
if (!config.find((item) => 'name' in item && item.name === 'name')) {
|
||
propsConfig.push({
|
||
name: 'name',
|
||
text: '组件名称',
|
||
});
|
||
}
|
||
|
||
const noCodeAdvancedTabItems = advancedTabConfig.items.filter(
|
||
(item) => 'type' in item && item.type !== 'code-select',
|
||
);
|
||
|
||
if (noCodeAdvancedTabItems.length > 0 && disabledCodeBlock) {
|
||
advancedTabConfig.items = noCodeAdvancedTabItems;
|
||
}
|
||
|
||
const tabConfig: TabConfig = {
|
||
type: 'tab',
|
||
labelWidth,
|
||
items: [
|
||
{
|
||
title: '属性',
|
||
items: [...propsConfig, ...config],
|
||
},
|
||
{ ...styleTabConfig },
|
||
{ ...eventTabConfig },
|
||
],
|
||
};
|
||
|
||
if (!disabledCodeBlock) {
|
||
tabConfig.items.push({ ...advancedTabConfig });
|
||
} else if (noCodeAdvancedTabItems.length > 0) {
|
||
tabConfig.items.push({ ...advancedTabConfig });
|
||
}
|
||
|
||
if (!disabledDataSource) {
|
||
tabConfig.items.push({ ...displayTabConfig });
|
||
}
|
||
|
||
return [tabConfig];
|
||
};
|
||
|
||
// #region ValidatePropsFormOptions
|
||
/**
|
||
* validatePropsForm 参数
|
||
*/
|
||
export interface ValidatePropsFormOptions {
|
||
/** 组件属性表单配置 */
|
||
config: FormConfig;
|
||
/** 待校验的表单值 */
|
||
values: FormValue;
|
||
/**
|
||
* 当前组件实例的 appContext(通常为 `getCurrentInstance()?.appContext`)。
|
||
* 会与 services 一并合入临时 MForm 的 appContext,使编辑器字段组件(DataSourceInput 等)能正常 inject。
|
||
*/
|
||
appContext?: AppContext | null;
|
||
/** 编辑器服务集合,注入到临时表单的 formState */
|
||
services?: Services;
|
||
/** stage 实例,注入到临时表单的 formState */
|
||
stage?: any;
|
||
/** 外部扩展的 formState */
|
||
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
||
/**
|
||
* 调试模式,默认 `true`:以弹层形式可见地渲染表单,点击「确定」才触发校验。
|
||
* 置为 `false` 时静默挂载后自动校验。
|
||
*/
|
||
debug?: boolean;
|
||
typeMatchValid?: boolean;
|
||
}
|
||
// #endregion ValidatePropsFormOptions
|
||
|
||
/**
|
||
* 对一份「组件属性表单配置 + 值」做一次独立的校验,**不复用也不污染页面上正在展示的表单**。
|
||
*
|
||
* 内部基于 `@tmagic/form` 的 `validateForm` 另建一个独立的 MForm 实例完成校验,并统一处理
|
||
* 编辑器场景所需的上下文注入:将当前组件实例的 provides 合入 appContext,并向 formState 注入
|
||
* stage / services 及外部扩展状态,保证校验规则依赖的上下文可用。
|
||
*
|
||
* 常用于源码编辑器保存后,对最新配置做一次校验,并将校验结果(错误信息)随提交一并抛给上层记录,
|
||
* 使源码保存的错误状态与表单编辑保持一致。
|
||
*
|
||
* @returns 校验通过返回空字符串 `''`,否则返回以 `<br>` 拼接的错误文案。
|
||
* 仅在初始化超时或挂载失败等异常情况下才会 reject。
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* const error = await validatePropsForm({
|
||
* config,
|
||
* values,
|
||
* appContext: getCurrentInstance()?.appContext,
|
||
* services,
|
||
* stage: editorService.get('stage'),
|
||
* extendState,
|
||
* });
|
||
* if (error) {
|
||
* // 配置不合法,error 为错误文案
|
||
* }
|
||
* ```
|
||
*/
|
||
export const validatePropsForm = ({
|
||
config,
|
||
values,
|
||
appContext = null,
|
||
services,
|
||
stage,
|
||
extendState,
|
||
debug,
|
||
typeMatchValid,
|
||
}: ValidatePropsFormOptions): Promise<string> =>
|
||
validateForm({
|
||
config,
|
||
debug,
|
||
typeMatchValid,
|
||
initValues: values,
|
||
// 将当前组件实例的 provides(含 Editor 顶层的 services / codeOptions 等组件级 provide)
|
||
// 合入 appContext,使临时 MForm 中的编辑器字段组件(DataSourceInput 等)能正常 inject
|
||
appContext: appContext ? { ...appContext, provides: { services } } : null,
|
||
// 与页面表单保持一致:注入 stage/services 及外部扩展状态,保证校验规则依赖的上下文可用
|
||
extendState: async (state) => ({
|
||
...((await extendState?.(state)) || {}),
|
||
stage,
|
||
services,
|
||
}),
|
||
});
|